├── README.md ├── ecclog.pl └── bitcoinolog.pl /README.md: -------------------------------------------------------------------------------- 1 | # Bitcoinolog: Reason about Bitcoin addresses with Prolog 2 | 3 | ## Offline Bitcoin wallet creation 4 | 5 | *Bitcoinolog* uses [Prolog](https://www.metalevel.at/prolog) and the 6 | [`crrl`](https://github.com/pornin/crrl/) crate to create Bitcoin 7 | addresses and private keys with several nice properties: 8 | 9 | - generated keys are **cryptographically secure** to the extent that 10 | `crrl` guarantees this property 11 | - the Prolog code is **short** and uses **no external programs** 12 | - keys can be generated **offline**, on a machine that has no 13 | Internet connection. 14 | 15 | Bitcoinolog requires Scryer Prolog. 16 | 17 | To try it, download [**`bitcoinolog.pl`**](bitcoinolog.pl) 18 | and run: 19 | 20 | $ scryer-prolog bitcoinolog.pl 21 | 22 | Here is an example query that you can try: 23 | 24 | ?- repeat, 25 | new_private_key(PrivateKey), 26 | private_key_to_public_key(PrivateKey, PublicKey), 27 | public_key_to_address(PublicKey, Address), 28 | private_key_to_wif(PrivateKey, WIF), 29 | format:portray_clause((address_key(A, K) :- A=Address, K=WIF)), 30 | false. 31 | 32 | This Prolog query *generates* Bitcoin addresses and private keys 33 | in Wallet Import Format (WIF), yielding: 34 | 35 | address_key(A, B) :- 36 | A="1Nis7V58Mb839kXb9RZMDzAN6ZTQbNfGC4", 37 | B="L1T3AnvHDtaAhkr9zxKokKzqphx26cvdoU8HbEifsWH6chy4bzYS". 38 | address_key(A, B) :- 39 | A="1D34yFJGRtiLuW2abwPBfks4cCi1Usp9a3", 40 | B="L5NUQyHr5zaJgG9ALbVGD1tcxodf51kUvQDANcNhpej1itV9KNLD". 41 | address_key(A, B) :- 42 | A="12TUkSL2yyiiAD2f4zAcGDQPev2FSQmVwF", 43 | B="KyR4Ut96DFadt1LbdZNFzCLuJ2Hw9KSeX6wfMv2dnQwgympWZyyU". 44 | address_key(A, B) :- 45 | A="1HySx6JBQKZoPqjUPVnbitFASKJjdAWWzx", 46 | B="Kxcr5G39Y7jUvsmsoDK5TnezsQ4FkTcYRVTcxzTT3uLYKnHFYiVh". 47 | 48 | For more information, visit: 49 | 50 | [**https://www.metalevel.at/bitcoinolog/**](https://www.metalevel.at/bitcoinolog/) 51 | 52 | **Video**: https://www.metalevel.at/prolog/videos/bitcoinolog 53 | 54 | ## Elliptic Curve Cryptography in Prolog 55 | 56 | Bitcoinolog uses 57 | [**`library(crypto)`**](https://github.com/mthom/scryer-prolog/blob/master/src/lib/crypto.pl) 58 | for hashing and reasoning over *elliptic curves*. 59 | 60 | Alternatively, you can use [**`ecclog.pl`**](ecclog.pl) for reasoning 61 | over elliptic curves *in Prolog*. Simply use the `ecc_` 62 | predicates instead of those starting with `crypto_`: 63 | 64 | - `crypto_curve_generator/2` → `ecc_curve_generator/2` 65 | - `crypto_curve_order/2` → `ecc_curve_order/2` 66 | - etc. 67 | 68 | Internally, `library(ecc)` uses 69 | [CLP(ℤ) constraints](https://www.metalevel.at/prolog/clpz) 70 | to facilitate 71 | [declarative debugging](https://www.metalevel.at/prolog/debugging). 72 | 73 | -------------------------------------------------------------------------------- /ecclog.pl: -------------------------------------------------------------------------------- 1 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 2 | ECClog: Elliptic Curve Cryptography with Prolog. 3 | 4 | Written June 2017 by Markus Triska (triska@metalevel.at) 5 | Tested with Scryer Prolog. 6 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 7 | 8 | :- module(ecclog, [ecc_name_curve/2, 9 | ecc_curve_generator/2, 10 | ecc_curve_order/2, 11 | ecc_curve_scalar_mult/4]). 12 | 13 | :- use_module(library(clpz)). 14 | :- use_module(library(arithmetic)). 15 | 16 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 17 | Sample use: Encryption and decryption of shared secret S. 18 | 19 | ?- ecc_name_curve(Name, C), 20 | ecc_curve_generator(C, Generator), 21 | PrivateKey = 10, 22 | ecc_curve_scalar_mult(C, PrivateKey, Generator, PublicKey), 23 | Random = 12, 24 | ecc_curve_scalar_mult(C, Random, Generator, R), 25 | ecc_curve_scalar_mult(C, Random, PublicKey, S), 26 | ecc_curve_scalar_mult(C, PrivateKey, R, S). 27 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 28 | 29 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 30 | Modular multiplicative inverse. 31 | 32 | Compute Y = X^(-1) mod p, using the extended Euclidean algorithm. 33 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 34 | 35 | multiplicative_inverse_modulo_p(X, P, Y) :- 36 | eea(X, P, _, _, Y), 37 | R #= X*Y mod P, 38 | zcompare(C, 1, R), 39 | must_be_one(C, X, P, Y). 40 | 41 | must_be_one(=, _, _, _). 42 | must_be_one(>, X, P, Y) :- throw(multiplicative_inverse_modulo_p(X,P,Y)). 43 | must_be_one(<, X, P, Y) :- throw(multiplicative_inverse_modulo_p(X,P,Y)). 44 | 45 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 46 | Extended Euclidean algorithm. 47 | 48 | Computes the GCD and the Bézout coefficients S and T. 49 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 50 | 51 | eea(I, J, G, S, T) :- 52 | State0 = state(1,0,0,1), 53 | eea_loop(I, J, State0, G, S, T). 54 | 55 | eea_loop(I, J, State0, G, S, T) :- 56 | zcompare(C, 0, J), 57 | eea_(C, I, J, State0, G, S, T). 58 | 59 | eea_(=, I, _, state(_,_,U,V), I, U, V). 60 | eea_(<, I0, J0, state(S0,T0,U0,V0), I, U, V) :- 61 | Q #= I0 // J0, 62 | R #= I0 mod J0, 63 | S1 #= U0 - (Q*S0), 64 | T1 #= V0 - (Q*T0), 65 | eea_loop(J0, R, state(S1,T1,S0,T0), I, U, V). 66 | 67 | 68 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 69 | Operations on Elliptic Curves 70 | ============================= 71 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 72 | 73 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 74 | An elliptic curve over a prime field F_p is represented as: 75 | 76 | curve(P,A,B,point(X,Y),Order,Cofactor). 77 | 78 | First, we define suitable accessors. 79 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 80 | 81 | curve_p(curve(P,_,_,_,_,_), P). 82 | curve_a(curve(_,A,_,_,_,_), A). 83 | curve_b(curve(_,_,B,_,_,_), B). 84 | 85 | ecc_curve_order(curve(_,_,_,_,Order,_), Order). 86 | ecc_curve_generator(curve(_,_,_,G,_,_), G). 87 | 88 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 89 | Scalar point multiplication. 90 | 91 | R = k*Q. 92 | 93 | The Montgomery ladder method is used to mitigate side-channel 94 | attacks such as timing attacks, since the number of multiplications 95 | and additions is independent of the private key K. This method does 96 | not even reveal the key's Hamming weight (number of 1s). 97 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 98 | 99 | ecc_curve_scalar_mult(Curve, K, Q, R) :- 100 | msb(K, Upper), 101 | scalar_multiplication(Curve, K, Upper, ml(null,Q)-R), 102 | must_be_on_curve(Curve, R). 103 | 104 | scalar_multiplication(Curve, K, I, R0-R) :- 105 | zcompare(C, -1, I), 106 | scalar_mult_(C, Curve, K, I, R0-R). 107 | 108 | scalar_mult_(=, _, _, _, ml(R,_)-R). 109 | scalar_mult_(<, Curve, K, I0, ML0-R) :- 110 | BitSet #= K /\ (1 << I0), 111 | zcompare(C, 0, BitSet), 112 | montgomery_step(C, Curve, ML0, ML1), 113 | I1 #= I0 - 1, 114 | scalar_multiplication(Curve, K, I1, ML1-R). 115 | 116 | montgomery_step(=, Curve, ml(R0,S0), ml(R,S)) :- 117 | curve_points_addition(Curve, R0, S0, S), 118 | curve_point_double(Curve, R0, R). 119 | montgomery_step(<, Curve, ml(R0,S0), ml(R,S)) :- 120 | curve_points_addition(Curve, R0, S0, R), 121 | curve_point_double(Curve, S0, S). 122 | 123 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 124 | Doubling a point: R = A + A. 125 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 126 | 127 | curve_point_double(_, null, null). 128 | curve_point_double(Curve, point(AX,AY), R) :- 129 | curve_p(Curve, P), 130 | curve_a(Curve, A), 131 | Numerator #= (3*AX^2 + A) mod P, 132 | Denom0 #= 2*AY mod P, 133 | multiplicative_inverse_modulo_p(Denom0, P, Denom), 134 | S #= (Numerator*Denom) mod P, 135 | R = point(RX,RY), 136 | RX #= (S^2 - 2*AX) mod P, 137 | RY #= (S*(AX - RX) - AY) mod P, 138 | must_be_on_curve(Curve, R). 139 | 140 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 141 | Adding two points. 142 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 143 | 144 | curve_points_addition(Curve, P, Q, R) :- 145 | curve_points_addition_(P, Curve, Q, R). 146 | 147 | curve_points_addition_(null, _, P, P). 148 | curve_points_addition_(P, _, null, P). 149 | curve_points_addition_(point(AX,AY), Curve, point(BX,BY), R) :- 150 | curve_p(Curve, P), 151 | Numerator #= (AY - BY) mod P, 152 | Denom0 #= (AX - BX) mod P, 153 | multiplicative_inverse_modulo_p(Denom0, P, Denom), 154 | S #= (Numerator * Denom) mod P, 155 | R = point(RX,RY), 156 | RX #= (S^2 - AX - BX) mod P, 157 | RY #= (S*(AX - RX) - AY) mod P, 158 | must_be_on_curve(Curve, R). 159 | 160 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 161 | Validation. 162 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 163 | 164 | curve_contains_point(Curve, point(QX,QY)) :- 165 | curve_a(Curve, A), 166 | curve_b(Curve, B), 167 | curve_p(Curve, P), 168 | QY^2 mod P #= (QX^3 + A*QX + B) mod P. 169 | 170 | must_be_on_curve(Curve, P) :- 171 | \+ curve_contains_point(Curve, P), 172 | throw(not_on_curve(P)). 173 | must_be_on_curve(Curve, P) :- curve_contains_point(Curve, P). 174 | 175 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 176 | Predefined curves 177 | ================= 178 | 179 | List available curves: 180 | 181 | $ openssl ecparam -list_curves 182 | 183 | Show curve parameters for secp256k1: 184 | 185 | $ openssl ecparam -param_enc explicit -conv_form uncompressed \ 186 | -text -no_seed -name secp256k1 187 | 188 | You must remove the leading "04:" from the generator. 189 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 190 | 191 | ecc_name_curve(secp112r1, 192 | curve(0x00db7c2abf62e35e668076bead208b, 193 | 0x00db7c2abf62e35e668076bead2088, 194 | 0x659ef8ba043916eede8911702b22, 195 | point(0x09487239995a5ee76b55f9c2f098, 196 | 0xa89ce5af8724c0a23e0e0ff77500), 197 | 0x00db7c2abf62e35e7628dfac6561c5, 198 | 1)). 199 | ecc_name_curve(secp256k1, 200 | curve(0x00fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f, 201 | 0x0, 202 | 0x7, 203 | point(0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798, 204 | 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8), 205 | 0x00fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141, 206 | 1)). 207 | -------------------------------------------------------------------------------- /bitcoinolog.pl: -------------------------------------------------------------------------------- 1 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 2 | Bitcoinolog: Reason about Bitcoin addresses with Prolog. 3 | 4 | Written 2017-2024 by Markus Triska (triska@metalevel.at) 5 | 6 | For more information, visit: 7 | 8 | https://www.metalevel.at/bitcoinolog/ 9 | ===================================== 10 | 11 | Tested with Scryer Prolog. 12 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 13 | 14 | :- module(bitcoinolog, [new_private_key/1, 15 | private_key_to_public_key/2, 16 | private_key_to_wif/2, 17 | public_key_to_address/2, 18 | hex_to_base58check/2, 19 | base58check_to_integer/2 20 | ]). 21 | 22 | :- use_module(library(clpz)). 23 | :- use_module(library(crypto)). 24 | :- use_module(library(dcgs)). 25 | :- use_module(library(error)). 26 | :- use_module(library(format)). 27 | :- use_module(library(lists)). 28 | :- use_module(library(between)). 29 | 30 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 31 | Sample use: Offline generation of Bitcoin addresses. 32 | 33 | ?- repeat, 34 | new_private_key(PrivateKey), 35 | private_key_to_public_key(PrivateKey, PublicKey), 36 | public_key_to_address(PublicKey, Address), 37 | format:portray_clause(Address), 38 | false. 39 | "142zbV8APtULexQai6NtrcSZxV9MoPwPPR". 40 | "13wh2Bhhc3B7M2k1ybWtSywBftrQ7Wm2wY". 41 | "1JxcrPifBFTUjZzYeFuPsWoaaZd5VR9yc". 42 | "12GDcpqLvgJiyPJRMqDFoZXvmx5LTn9uaK". 43 | 44 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 45 | 46 | bitcoin_curve(Curve) :- 47 | crypto_name_curve(secp256k1, Curve). 48 | 49 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 50 | Generate a cryptographically secure random integer between 0 and 2^256. 51 | 52 | Almost all such integers can be used as *private keys* for Bitcoin. 53 | 54 | If an integer outside the suitable range is generated, an exception 55 | is thrown. The chances of this happening are extremely low. 56 | 57 | Sample use: 58 | 59 | ?- new_private_key(K). 60 | %@ K = 39615274996621223576805938914018474719127698794300917857701300837868839491832. 61 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 62 | 63 | new_private_key(Key) :- 64 | crypto_n_random_bytes(32, Bytes), 65 | hex_bytes(Hex, Bytes), 66 | hex_to_integer(Hex, Key), 67 | bitcoin_curve(Curve), 68 | crypto_curve_order(Curve, Order), 69 | Upper #= Order - 1, 70 | ( between(1, Upper, Key) -> true 71 | ; domain_error(private_key, Key, new_private_key/1) 72 | ). 73 | 74 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 75 | A public key is a point on the curve, with coordinates (X,Y). In 76 | ECDSA, the public key can be derived from the private key by 77 | multiplying the generator with the private key. 78 | 79 | Deriving a public key and address is described in: 80 | 81 | https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses 82 | 83 | We deviate from this description in step (2) in that we store 84 | public keys in *compressed* form. This means that we use Prefix++X, 85 | where Prefix is either 0x02 or 0x03 depending on whether Y is even 86 | or odd, respectively. 87 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 88 | 89 | private_key_to_public_key(PrivateKey, PublicKey) :- 90 | bitcoin_curve(Curve), 91 | crypto_curve_generator(Curve, Generator), 92 | % 1. Compute the public key as PrivateKey*Generator. 93 | crypto_curve_scalar_mult(Curve, PrivateKey, Generator, point(X,Y)), 94 | % 2. PublicKey in compressed form. 95 | Rem #= Y mod 2, 96 | zcompare(Cmp, 0, Rem), 97 | cmp0_prefix(Cmp, Prefix), 98 | phrase(format_("~s~|~`0t~16r~64+", [Prefix,X]), PublicKey). 99 | 100 | cmp0_prefix(=, "02"). 101 | cmp0_prefix(<, "03"). 102 | 103 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 104 | Convert a private key to Wallet Import Format (WIF). 105 | 106 | This is how private keys are typically shown in wallets. 107 | 108 | The prefix 80 is used for private keys. 01 is appended to indicate 109 | that these private keys correspond to *compressed* public keys. 110 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 111 | 112 | private_key_to_wif(PrivateKey0, WIF) :- 113 | phrase(format_("80~|~`0t~16r~64+01", [PrivateKey0]), PrivateKey), 114 | % Twice perform SHA-256 on the hex encoding of the private key. 115 | hex_algorithm_hash(PrivateKey, sha256, HashPrivateKey1), 116 | hex_algorithm_hash(HashPrivateKey1, sha256, HashPrivateKey2), 117 | % Take the first four bytes, which are the checksum. 118 | hex_bytes(HashPrivateKey2, Bytes), 119 | Bytes = [B1,B2,B3,B4|_], 120 | hex_bytes(PrivateKey, PrivateKeyBytes), 121 | append(PrivateKeyBytes, [B1,B2,B3,B4], WIF0), 122 | % Convert the address to Base58Check encoding. 123 | hex_bytes(WIF1, WIF0), 124 | hex_to_base58check(WIF1, WIF). 125 | 126 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 127 | Convert a public key to a Bitcoin address in Base58Check encoding. 128 | This encoding is commonly shown for Bitcoin addresses. 129 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 130 | 131 | public_key_to_address(PublicKey, Address) :- 132 | % 1. Compute public key from private key: already done! 133 | % 2. Perform SHA-256 on the hex encoding of the public key. 134 | hex_algorithm_hash(PublicKey, sha256, HashPublicKey), 135 | % 3. Perform RIPEMD-160 hashing on the result. 136 | hex_algorithm_hash(HashPublicKey, ripemd160, RIPEMD160), 137 | % 4. Add version byte in front of RIPEMD-160. 138 | % 0x00 denotes Main Network. 139 | append("00", RIPEMD160, ExtendedRIPE), 140 | % 5. Perform SHA-256 on the extended RIPEMD-160 result. 141 | hex_algorithm_hash(ExtendedRIPE, sha256, SHA256_1), 142 | % 6. Perform SHA-256 on the previous result. 143 | hex_algorithm_hash(SHA256_1, sha256, SHA256_2), 144 | % 7. Take the first four bytes, which are the checksum. 145 | hex_bytes(SHA256_2, Bytes), 146 | Bytes = [B1,B2,B3,B4|_], 147 | % 8. Add the 4 checksum bytes at the end of extended RIPEMD-160. 148 | % This is the 25-byte binary Bitcoin Address. 149 | hex_bytes(ExtendedRIPE, ExtendedBytes), 150 | append(ExtendedBytes, [B1,B2,B3,B4], Address0), 151 | % 9. Convert the address to Base58Check encoding. 152 | hex_bytes(Address1, Address0), 153 | hex_to_base58check(Address1, Address). 154 | 155 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 156 | Compute the hash of a hex code, using Algorithm. 157 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 158 | 159 | hex_algorithm_hash(Hex, Algorithm, Hash) :- 160 | hex_bytes(Hex, Bytes), 161 | maplist(char_code, Chars, Bytes), 162 | crypto_data_hash(Chars, Hash, [algorithm(Algorithm), 163 | encoding(octet)]). 164 | 165 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 166 | Base58Check format. 167 | 168 | The most important conversion predicate is hex_to_base58check/2. 169 | The predicate base58check_to_integer/2 can be useful too. 170 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 171 | 172 | base58_alphabet("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"). 173 | 174 | hex_to_base58check(Hex, Base58Check) :- 175 | hex_to_integer(Hex, Integer), 176 | phrase(base58check(Integer), Bs0), 177 | reverse(Bs0, Bs), 178 | hex_bytes(Hex, Bytes), 179 | once(phrase(leading_zeroes(Bytes), Base58Check, Bs)). 180 | 181 | leading_zeroes([]) --> []. 182 | leading_zeroes([0|Rest]) --> "1", leading_zeroes(Rest). 183 | leading_zeroes([D|_]) --> { D #\= 0 }. 184 | 185 | base58check(I0) --> 186 | { zcompare(C, 0, I0) }, 187 | base58check_(C, I0). 188 | 189 | base58check_(=, _) --> []. 190 | base58check_(<, I0) --> [Char], 191 | { I #= I0 // 58, 192 | Remainder #= I0 mod 58, 193 | base58_alphabet(As), 194 | nth0(Remainder, As, Char) }, 195 | base58check(I). 196 | 197 | base58check_to_integer(B58, Integer) :- 198 | foldl(base58_times58, B58, 0, Integer). 199 | 200 | base58_times58(Char, S0, S) :- 201 | base58_alphabet(As), 202 | nth0(Value, As, Char), 203 | S #= S0*58 + Value. 204 | 205 | hex_to_integer(Hex, Integer) :- 206 | hex_bytes(Hex, Bytes), 207 | foldl(plus_times256, Bytes, 0, Integer). 208 | 209 | plus_times256(B, S0, S) :- S #= S0*256 + B. 210 | --------------------------------------------------------------------------------