diff --git a/basicswap/contrib/blake256/blake256.py b/basicswap/contrib/blake256/blake256.py new file mode 100644 index 0000000..51e2b0f --- /dev/null +++ b/basicswap/contrib/blake256/blake256.py @@ -0,0 +1,533 @@ + + +intro = """ + blake.py + version 5, 2-Apr-2014 + + BLAKE is a SHA3 round-3 finalist designed and submitted by + Jean-Philippe Aumasson et al. + + At the core of BLAKE is a ChaCha-like mixer, very similar + to that found in the stream cipher, ChaCha8. Besides being + a very good mixer, ChaCha is fast. + + References: + http://www.131002.net/blake/ + http://csrc.nist.gov/groups/ST/hash/sha-3/index.html + http://en.wikipedia.org/wiki/BLAKE_(hash_function) + + This implementation assumes all data is in increments of + whole bytes. (The formal definition of BLAKE allows for + hashing individual bits.) Note too that this implementation + does include the round-3 tweaks where the number of rounds + was increased to 14/16 from 10/14. + + This version can be imported into both Python2 (2.6 and 2.7) + and Python3 programs. Python 2.5 requires an older version + of blake.py (version 4). + + Here are some comparative times for different versions of + Python: + + 64-bit: + 2.6 6.284s + 2.7 6.343s + 3.2 7.620s + pypy (2.7) 2.080s + + 32-bit: + 2.5 (32) 15.389s (with psyco) + 2.7-32 13.645s + 3.2-32 12.574s + + One test on a 2.0GHz Core 2 Duo of 10,000 iterations of + BLAKE-256 on a short message produced a time of 5.7 seconds. + Not bad, but if raw speed is what you want, look to the + the C version. It is 40x faster and did the same thing + in 0.13 seconds. + + Copyright (c) 2009-2012 by Larry Bugbee, Kent, WA + ALL RIGHTS RESERVED. + + blake.py IS EXPERIMENTAL SOFTWARE FOR EDUCATIONAL + PURPOSES ONLY. IT IS MADE AVAILABLE "AS-IS" WITHOUT + WARRANTY OR GUARANTEE OF ANY KIND. USE SIGNIFIES + ACCEPTANCE OF ALL RISK. + + To make your learning and experimentation less cumbersome, + blake.py is free for any use. + + Enjoy, + + Larry Bugbee + March 2011 + rev May 2011 - fixed Python version check (tx JP) + rev Apr 2012 - fixed an out-of-order bit set in final() + - moved self-test to a separate test pgm + - this now works with Python2 and Python3 + rev Apr 2014 - added test and conversion of string input + to byte string in update() (tx Soham) + - added hexdigest() method. + - now support state 3 so only one call to + final() per instantiation is allowed. all + subsequent calls to final(), digest() or + hexdigest() simply return the stored value. + +""" + +import struct +from binascii import hexlify, unhexlify + +#--------------------------------------------------------------- + +class BLAKE(object): + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - + # initial values, constants and padding + + # IVx for BLAKE-x + + IV64 = [ + 0x6A09E667F3BCC908, 0xBB67AE8584CAA73B, + 0x3C6EF372FE94F82B, 0xA54FF53A5F1D36F1, + 0x510E527FADE682D1, 0x9B05688C2B3E6C1F, + 0x1F83D9ABFB41BD6B, 0x5BE0CD19137E2179, + ] + + IV48 = [ + 0xCBBB9D5DC1059ED8, 0x629A292A367CD507, + 0x9159015A3070DD17, 0x152FECD8F70E5939, + 0x67332667FFC00B31, 0x8EB44A8768581511, + 0xDB0C2E0D64F98FA7, 0x47B5481DBEFA4FA4, + ] + + # note: the values here are the same as the high-order + # half-words of IV64 + IV32 = [ + 0x6A09E667, 0xBB67AE85, + 0x3C6EF372, 0xA54FF53A, + 0x510E527F, 0x9B05688C, + 0x1F83D9AB, 0x5BE0CD19, + ] + + # note: the values here are the same as the low-order + # half-words of IV48 + IV28 = [ + 0xC1059ED8, 0x367CD507, + 0x3070DD17, 0xF70E5939, + 0xFFC00B31, 0x68581511, + 0x64F98FA7, 0xBEFA4FA4, + ] + + # constants for BLAKE-64 and BLAKE-48 + C64 = [ + 0x243F6A8885A308D3, 0x13198A2E03707344, + 0xA4093822299F31D0, 0x082EFA98EC4E6C89, + 0x452821E638D01377, 0xBE5466CF34E90C6C, + 0xC0AC29B7C97C50DD, 0x3F84D5B5B5470917, + 0x9216D5D98979FB1B, 0xD1310BA698DFB5AC, + 0x2FFD72DBD01ADFB7, 0xB8E1AFED6A267E96, + 0xBA7C9045F12C7F99, 0x24A19947B3916CF7, + 0x0801F2E2858EFC16, 0x636920D871574E69, + ] + + # constants for BLAKE-32 and BLAKE-28 + # note: concatenate and the values are the same as the values + # for the 1st half of C64 + C32 = [ + 0x243F6A88, 0x85A308D3, + 0x13198A2E, 0x03707344, + 0xA4093822, 0x299F31D0, + 0x082EFA98, 0xEC4E6C89, + 0x452821E6, 0x38D01377, + 0xBE5466CF, 0x34E90C6C, + 0xC0AC29B7, 0xC97C50DD, + 0x3F84D5B5, 0xB5470917, + ] + + # the 10 permutations of:0,...15} + SIGMA = [ + [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15], + [14,10, 4, 8, 9,15,13, 6, 1,12, 0, 2,11, 7, 5, 3], + [11, 8,12, 0, 5, 2,15,13,10,14, 3, 6, 7, 1, 9, 4], + [ 7, 9, 3, 1,13,12,11,14, 2, 6, 5,10, 4, 0,15, 8], + [ 9, 0, 5, 7, 2, 4,10,15,14, 1,11,12, 6, 8, 3,13], + [ 2,12, 6,10, 0,11, 8, 3, 4,13, 7, 5,15,14, 1, 9], + [12, 5, 1,15,14,13, 4,10, 0, 7, 6, 3, 9, 2, 8,11], + [13,11, 7,14,12, 1, 3, 9, 5, 0,15, 4, 8, 6, 2,10], + [ 6,15,14, 9,11, 3, 0, 8,12, 2,13, 7, 1, 4,10, 5], + [10, 2, 8, 4, 7, 6, 1, 5,15,11, 9,14, 3,12,13, 0], + [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15], + [14,10, 4, 8, 9,15,13, 6, 1,12, 0, 2,11, 7, 5, 3], + [11, 8,12, 0, 5, 2,15,13,10,14, 3, 6, 7, 1, 9, 4], + [ 7, 9, 3, 1,13,12,11,14, 2, 6, 5,10, 4, 0,15, 8], + [ 9, 0, 5, 7, 2, 4,10,15,14, 1,11,12, 6, 8, 3,13], + [ 2,12, 6,10, 0,11, 8, 3, 4,13, 7, 5,15,14, 1, 9], + [12, 5, 1,15,14,13, 4,10, 0, 7, 6, 3, 9, 2, 8,11], + [13,11, 7,14,12, 1, 3, 9, 5, 0,15, 4, 8, 6, 2,10], + [ 6,15,14, 9,11, 3, 0, 8,12, 2,13, 7, 1, 4,10, 5], + [10, 2, 8, 4, 7, 6, 1, 5,15,11, 9,14, 3,12,13, 0], + ] + + MASK32BITS = 0xFFFFFFFF + MASK64BITS = 0xFFFFFFFFFFFFFFFF + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - + + def __init__(self, hashbitlen): + """ + load the hashSate structure (copy hashbitlen...) + hashbitlen: length of the hash output + """ + if hashbitlen not in [224, 256, 384, 512]: + raise Exception('hash length not 224, 256, 384 or 512') + + self.hashbitlen = hashbitlen + self.h = [0]*8 # current chain value (initialized to the IV) + self.t = 0 # number of *BITS* hashed so far + self.cache = b'' # cached leftover data not yet compressed + self.salt = [0]*4 # salt (null by default) + self.state = 1 # set to 2 by update and 3 by final + self.nullt = 0 # Boolean value for special case \ell_i=0 + + # The algorithm is the same for both the 32- and 64- versions + # of BLAKE. The difference is in word size (4 vs 8 bytes), + # blocksize (64 vs 128 bytes), number of rounds (14 vs 16) + # and a few very specific constants. + if (hashbitlen == 224) or (hashbitlen == 256): + # setup for 32-bit words and 64-bit block + self.byte2int = self._fourByte2int + self.int2byte = self._int2fourByte + self.MASK = self.MASK32BITS + self.WORDBYTES = 4 + self.WORDBITS = 32 + self.BLKBYTES = 64 + self.BLKBITS = 512 + self.ROUNDS = 14 # was 10 before round 3 + self.cxx = self.C32 + self.rot1 = 16 # num bits to shift in G + self.rot2 = 12 # num bits to shift in G + self.rot3 = 8 # num bits to shift in G + self.rot4 = 7 # num bits to shift in G + self.mul = 0 # for 32-bit words, 32<>1 is the same as i/2 but faster) + v[12] = v[12] ^ (self.t & MASK) + v[13] = v[13] ^ (self.t & MASK) + v[14] = v[14] ^ (self.t >> self.WORDBITS) + v[15] = v[15] ^ (self.t >> self.WORDBITS) + + # - - - - - - - - - - - - - - - - - + # ready? let's ChaCha!!! + + def G(a, b, c, d, i): + va = v[a] # it's faster to deref and reref later + vb = v[b] + vc = v[c] + vd = v[d] + + sri = SIGMA[round][i] + sri1 = SIGMA[round][i+1] + + va = ((va + vb) + (m[sri] ^ cxx[sri1]) ) & MASK + x = vd ^ va + vd = (x >> rot1) | ((x << (WORDBITS-rot1)) & MASK) + vc = (vc + vd) & MASK + x = vb ^ vc + vb = (x >> rot2) | ((x << (WORDBITS-rot2)) & MASK) + + va = ((va + vb) + (m[sri1] ^ cxx[sri]) ) & MASK + x = vd ^ va + vd = (x >> rot3) | ((x << (WORDBITS-rot3)) & MASK) + vc = (vc + vd) & MASK + x = vb ^ vc + vb = (x >> rot4) | ((x << (WORDBITS-rot4)) & MASK) + + v[a] = va + v[b] = vb + v[c] = vc + v[d] = vd + + for round in range(self.ROUNDS): + # column step + G( 0, 4, 8,12, 0) + G( 1, 5, 9,13, 2) + G( 2, 6,10,14, 4) + G( 3, 7,11,15, 6) + # diagonal step + G( 0, 5,10,15, 8) + G( 1, 6,11,12,10) + G( 2, 7, 8,13,12) + G( 3, 4, 9,14,14) + + # - - - - - - - - - - - - - - - - - + + # save current hash value (use i&0x3 to get 0,1,2,3,0,1,2,3) + self.h = [self.h[i]^v[i]^v[i+8]^self.salt[i&0x3] + for i in range(8)] + # print 'self.h', [num2hex(h) for h in self.h] + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - + + def addsalt(self, salt): + """ adds a salt to the hash function (OPTIONAL) + should be called AFTER Init, and BEFORE update + salt: a bytestring, length determined by hashbitlen. + if not of sufficient length, the bytestring + will be assumed to be a big endian number and + prefixed with an appropriate number of null + bytes, and if too large, only the low order + bytes will be used. + + if hashbitlen=224 or 256, then salt will be 16 bytes + if hashbitlen=384 or 512, then salt will be 32 bytes + """ + # fail if addsalt() was not called at the right time + if self.state != 1: + raise Exception('addsalt() not called after init() and before update()') + # salt size is to be 4x word size + saltsize = self.WORDBYTES * 4 + # if too short, prefix with null bytes. if too long, + # truncate high order bytes + if len(salt) < saltsize: + salt = (chr(0)*(saltsize-len(salt)) + salt) + else: + salt = salt[-saltsize:] + # prep the salt array + self.salt[0] = self.byte2int(salt[ : 4<= fill: + self.cache = self.cache + data[:fill] + self.t += BLKBITS # update counter + self._compress(self.cache) + self.cache = b'' + data = data[fill:] + datalen -= fill + + # compress new data until not enough for a full block + while datalen >= BLKBYTES: + self.t += BLKBITS # update counter + self._compress(data[:BLKBYTES]) + data = data[BLKBYTES:] + datalen -= BLKBYTES + + # cache all leftover bytes until next call to update() + if datalen > 0: + self.cache = self.cache + data[:datalen] + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - + + def final(self, data=''): + """ finalize the hash -- pad and hash remaining data + returns hashval, the digest + """ + if self.state == 3: + # we have already finalized so simply return the + # previously calculated/stored hash value + return self.hash + + if data: + self.update(data) + + ZZ = b'\x00' + ZO = b'\x01' + OZ = b'\x80' + OO = b'\x81' + PADDING = OZ + ZZ*128 # pre-formatted padding data + + # copy nb. bits hash in total as a 64-bit BE word + # copy nb. bits hash in total as a 128-bit BE word + tt = self.t + (len(self.cache) << 3) + if self.BLKBYTES == 64: + msglen = self._int2eightByte(tt) + else: + low = tt & self.MASK + high = tt >> self.WORDBITS + msglen = self._int2eightByte(high) + self._int2eightByte(low) + + # size of block without the words at the end that count + # the number of bits, 55 or 111. + # Note: (((self.WORDBITS/8)*2)+1) equals ((self.WORDBITS>>2)+1) + sizewithout = self.BLKBYTES - ((self.WORDBITS>>2)+1) + + if len(self.cache) == sizewithout: + # special case of one padding byte + self.t -= 8 + if self.hashbitlen in [224, 384]: + self.update(OZ) + else: + self.update(OO) + else: + if len(self.cache) < sizewithout: + # enough space to fill the block + # use t=0 if no remaining data + if len(self.cache) == 0: + self.nullt=1 + self.t -= (sizewithout - len(self.cache)) << 3 + self.update(PADDING[:sizewithout - len(self.cache)]) + else: + # NOT enough space, need 2 compressions + # ...add marker, pad with nulls and compress + self.t -= (self.BLKBYTES - len(self.cache)) << 3 + self.update(PADDING[:self.BLKBYTES - len(self.cache)]) + # ...now pad w/nulls leaving space for marker & bit count + self.t -= (sizewithout+1) << 3 + self.update(PADDING[1:sizewithout+1]) # pad with zeroes + + self.nullt = 1 # raise flag to set t=0 at the next _compress + + # append a marker byte + if self.hashbitlen in [224, 384]: + self.update(ZZ) + else: + self.update(ZO) + self.t -= 8 + + # append the number of bits (long long) + self.t -= self.BLKBYTES + self.update(msglen) + + hashval = [] + if self.BLKBYTES == 64: + for h in self.h: + hashval.append(self._int2fourByte(h)) + else: + for h in self.h: + hashval.append(self._int2eightByte(h)) + + self.hash = b''.join(hashval)[:self.hashbitlen >> 3] + self.state = 3 + + return self.hash + + digest = final # may use digest() as a synonym for final() + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - + + def hexdigest(self, data=''): + return hexlify(self.final(data)).decode('UTF-8') + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + # utility functions + + def _fourByte2int(self, bytestr): # see also long2byt() below + """ convert a 4-byte string to an int (long) """ + return struct.unpack('!L', bytestr)[0] + + def _eightByte2int(self, bytestr): + """ convert a 8-byte string to an int (long long) """ + return struct.unpack('!Q', bytestr)[0] + + def _int2fourByte(self, x): # see also long2byt() below + """ convert a number to a 4-byte string, high order + truncation possible (in Python x could be a BIGNUM) + """ + return struct.pack('!L', x) + + def _int2eightByte(self, x): + """ convert a number to a 8-byte string, high order + truncation possible (in Python x could be a BIGNUM) + """ + return struct.pack('!Q', x) + + +#--------------------------------------------------------------- +#--------------------------------------------------------------- +#--------------------------------------------------------------- + + +def blake_hash(data): + return BLAKE(256).digest(data) \ No newline at end of file diff --git a/basicswap/contrib/blake256/test.py b/basicswap/contrib/blake256/test.py new file mode 100644 index 0000000..b7c3b97 --- /dev/null +++ b/basicswap/contrib/blake256/test.py @@ -0,0 +1,37 @@ +from blake256 import blake_hash + +testVectors = [ + ["716f6e863f744b9ac22c97ec7b76ea5f5908bc5b2f67c61510bfc4751384ea7a", ""], + ["43234ff894a9c0590d0246cfc574eb781a80958b01d7a2fa1ac73c673ba5e311", "a"], + ["658c6d9019a1deddbcb3640a066dfd23471553a307ab941fd3e677ba887be329", "ab"], + ["1833a9fa7cf4086bd5fda73da32e5a1d75b4c3f89d5c436369f9d78bb2da5c28", "abc"], + ["35282468f3b93c5aaca6408582fced36e578f67671ed0741c332d68ac72d7aa2", "abcd"], + ["9278d633efce801c6aa62987d7483d50e3c918caed7d46679551eed91fba8904", "abcde"], + ["7a17ee5e289845adcafaf6ca1b05c4a281b232a71c7083f66c19ba1d1169a8d4", "abcdef"], + ["ee8c7f94ff805cb2e644643010ea43b0222056420917ec70c3da764175193f8f", "abcdefg"], + ["7b37c0876d29c5add7800a1823795a82b809fc12f799ff6a4b5e58d52c42b17e", "abcdefgh"], + ["bdc514bea74ffbb9c3aa6470b08ceb80a88e313ad65e4a01457bbffd0acc86de", "abcdefghi"], + ["12e3afb9739df8d727e93d853faeafc374cc55aedc937e5a1e66f5843b1d4c2e", "abcdefghij"], + ["22297d373b751f581944bb26315133f6fda2f0bf60f65db773900f61f81b7e79", "Discard medicine more than two years old."], + ["4d48d137bc9cf6d21415b805bf33f59320337d85c673998260e03a02a0d760cd", "He who has a shady past knows that nice guys finish last."], + ["beba299e10f93e17d45663a6dc4b8c9349e4f5b9bac0d7832389c40a1b401e5c", "I wouldn't marry him with a ten foot pole."], + ["42e082ae7f967781c6cd4e0ceeaeeb19fb2955adbdbaf8c7ec4613ac130071b3", "Free! Free!/A trip/to Mars/for 900/empty jars/Burma Shave"], + ["207d06b205bfb359df91b48b6fd8aa6e4798b712d1cc5e91a254da9cef8684a3", "The days of the digital watch are numbered. -Tom Stoppard"], + ["d56eab6927e371e2148b0788779aaf565d30567af2af822b6be3b90db9767a70", "Nepal premier won't resign."], + ["01020709ca7fd10dc7756ce767d508d7206167d300b7a7ed76838a8547a7898c", "For every action there is an equal and opposite government program."], + ["5569a6cc6535a66da221d8f6ad25008f28752d0343f3f1d757f1ecc9b1c61536", "His money is twice tainted: 'taint yours and 'taint mine."], + ["8ff699b5ac7687c82600e89d0ff6cfa87e7179759184386971feb76fbae9975f", "There is no reason for any individual to have a computer in their home. -Ken Olsen, 1977"], + ["f4b3a7c85a418b15ce330fd41ae0254b036ad48dd98aa37f0506a995ba9c6029", "It's a tiny change to the code and not completely disgusting. - Bob Manchek"], + ["1ed94bab64fe560ef0983165fcb067e9a8a971c1db8e6fb151ff9a7c7fe877e3", "size: a.out: bad magic"], + ["ff15b54992eedf9889f7b4bbb16692881aa01ed10dfc860fdb04785d8185cd3c", "The major problem is with sendmail. -Mark Horton"], + ["8a0a7c417a47deec0b6474d8c247da142d2e315113a2817af3de8f45690d8652", "Give me a rock, paper and scissors and I will move the world. CCFestoon"], + ["310d263fdab056a930324cdea5f46f9ea70219c1a74b01009994484113222a62", "If the enemy is within range, then so are you."], + ["1aaa0903aa4cf872fe494c322a6e535698ea2140e15f26fb6088287aedceb6ba", "It's well we cannot hear the screams/That we create in others' dreams."], + ["2eb81bcaa9e9185a7587a1b26299dcfb30f2a58a7f29adb584b969725457ad4f", "You remind me of a TV show, but that's all right: I watch it anyway."], + ["c27b1683ef76e274680ab5492e592997b0d9d5ac5a5f4651b6036f64215256af", "C is as portable as Stonehedge!!"], + ["3995cce8f32b174c22ffac916124bd095c80205d9d5f1bb08a155ac24b40d6cb", "Even if I could be Shakespeare, I think I should still choose to be Faraday. - A. Huxley"], + ["496f7063f8bd479bf54e9d87e9ba53e277839ac7fdaecc5105f2879b58ee562f", "The fugacity of a constituent in a mixture of gases at a given temperature is proportional to its mole fraction. Lewis-Randall Rule"], + ["2e0eff918940b01eea9539a02212f33ee84f77fab201f4287aa6167e4a1ed043", "How can you write a big system without C++? -Paul Glick"]] + +for vectorSet in testVectors: + assert vectorSet[0] == blake_hash(vectorSet[1]).encode('hex') \ No newline at end of file diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index b3debc5..e9b3e6e 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -37,6 +37,9 @@ from basicswap.util.address import ( decodeAddress, pubkeyToAddress, ) +from basicswap.util.crypto import ( + sha256, +) from coincurve.keys import ( PrivateKey, PublicKey) @@ -109,11 +112,27 @@ def find_vout_for_address_from_txobj(tx_obj, addr: str) -> int: raise RuntimeError("Vout not found for address: txid={}, addr={}".format(tx_obj['txid'], addr)) -class BTCInterface(CoinInterface): +class Secp256k1Interface(CoinInterface): @staticmethod def curve_type(): return Curves.secp256k1 + def getNewSecretKey(self) -> bytes: + return i2b(getSecretInt()) + + def getPubkey(self, privkey): + return PublicKey.from_secret(privkey).format() + + def verifyKey(self, k: bytes) -> bool: + i = b2i(k) + return (i < ep.o and i > 0) + + def verifyPubkey(self, pubkey_bytes: bytes) -> bool: + return verify_secp256k1_point(pubkey_bytes) + + +class BTCInterface(Secp256k1Interface): + @staticmethod def coin_type(): return Coins.BTC @@ -422,11 +441,11 @@ class BTCInterface(CoinInterface): return segwit_addr.encode(bech32_prefix, version, pkh) def pkh_to_address(self, pkh: bytes) -> str: - # pkh is hash160(pk) + # pkh is ripemd160(sha256(pk)) assert (len(pkh) == 20) prefix = self.chainparams_network()['pubkey_address'] data = bytes((prefix,)) + pkh - checksum = hashlib.sha256(hashlib.sha256(data).digest()).digest() + checksum = sha256(sha256(data)) return b58encode(data + checksum[0:4]) def sh_to_address(self, sh: bytes) -> str: @@ -452,12 +471,6 @@ class BTCInterface(CoinInterface): assert (len(pk) == 33) return self.pkh_to_address(hash160(pk)) - def getNewSecretKey(self) -> bytes: - return i2b(getSecretInt()) - - def getPubkey(self, privkey): - return PublicKey.from_secret(privkey).format() - def getAddressHashFromKey(self, key: bytes) -> bytes: pk = self.getPubkey(key) return hash160(pk) @@ -465,13 +478,6 @@ class BTCInterface(CoinInterface): def getSeedHash(self, seed) -> bytes: return self.getAddressHashFromKey(seed)[::-1] - def verifyKey(self, k: bytes) -> bool: - i = b2i(k) - return (i < ep.o and i > 0) - - def verifyPubkey(self, pubkey_bytes: bytes) -> bool: - return verify_secp256k1_point(pubkey_bytes) - def encodeKey(self, key_bytes: bytes) -> str: wif_prefix = self.chainparams_network()['key_prefix'] return toWIF(wif_prefix, key_bytes) @@ -1018,20 +1024,20 @@ class BTCInterface(CoinInterface): return hash160(K) def getScriptDest(self, script): - return CScript([OP_0, hashlib.sha256(script).digest()]) + return CScript([OP_0, sha256(script)]) def getScriptScriptSig(self, script: bytes) -> bytes: return bytes() def getP2SHP2WSHDest(self, script): - script_hash = hashlib.sha256(script).digest() + script_hash = sha256(script) assert len(script_hash) == 32 p2wsh_hash = hash160(CScript([OP_0, script_hash])) assert len(p2wsh_hash) == 20 return CScript([OP_HASH160, p2wsh_hash, OP_EQUAL]) def getP2SHP2WSHScriptSig(self, script): - script_hash = hashlib.sha256(script).digest() + script_hash = sha256(script) assert len(script_hash) == 32 return CScript([CScript([OP_0, script_hash, ]), ]) @@ -1249,25 +1255,25 @@ class BTCInterface(CoinInterface): return self.rpc_wallet('sendtoaddress', params) def signCompact(self, k, message): - message_hash = hashlib.sha256(bytes(message, 'utf-8')).digest() + message_hash = sha256(bytes(message, 'utf-8')) privkey = PrivateKey(k) return privkey.sign_recoverable(message_hash, hasher=None)[:64] def signRecoverable(self, k, message): - message_hash = hashlib.sha256(bytes(message, 'utf-8')).digest() + message_hash = sha256(bytes(message, 'utf-8')) privkey = PrivateKey(k) return privkey.sign_recoverable(message_hash, hasher=None) def verifyCompactSig(self, K, message, sig): - message_hash = hashlib.sha256(bytes(message, 'utf-8')).digest() + message_hash = sha256(bytes(message, 'utf-8')) pubkey = PublicKey(K) rv = pubkey.verify_compact(sig, message_hash, hasher=None) assert (rv is True) def verifySigAndRecover(self, sig, message): - message_hash = hashlib.sha256(bytes(message, 'utf-8')).digest() + message_hash = sha256(bytes(message, 'utf-8')) pubkey = PublicKey.from_signature_and_message(sig, message_hash, hasher=None) return pubkey.format() @@ -1276,7 +1282,7 @@ class BTCInterface(CoinInterface): message_magic = self.chainparams()['message_magic'] message_bytes = SerialiseNumCompact(len(message_magic)) + bytes(message_magic, 'utf-8') + SerialiseNumCompact(len(message)) + bytes(message, 'utf-8') - message_hash = hashlib.sha256(hashlib.sha256(message_bytes).digest()).digest() + message_hash = sha256(sha256(message_bytes)) signature_bytes = base64.b64decode(signature) rec_id = (signature_bytes[0] - 27) & 3 signature_bytes = signature_bytes[1:] + bytes((rec_id,)) @@ -1502,7 +1508,7 @@ class BTCInterface(CoinInterface): return CScript([OP_HASH160, script_hash, OP_EQUAL]) def get_p2wsh_script_pubkey(self, script: bytearray) -> bytearray: - return CScript([OP_0, hashlib.sha256(script).digest()]) + return CScript([OP_0, sha256(script)]) def findTxnByHash(self, txid_hex: str): # Only works for wallet txns diff --git a/basicswap/interface/dcr.py b/basicswap/interface/dcr.py new file mode 100644 index 0000000..f966ef2 --- /dev/null +++ b/basicswap/interface/dcr.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +from basicswap.chainparams import Coins +from basicswap.interface.btc import Secp256k1Interface +from basicswap.util.address import ( + b58decode, + b58encode, +) +from basicswap.util.crypto import ( + blake256, + ripemd160, +) + + +class DCRInterface(Secp256k1Interface): + + @staticmethod + def coin_type(): + return Coins.DCR + + @staticmethod + def exp() -> int: + return 8 + + @staticmethod + def COIN() -> int: + return 100000000 + + @staticmethod + def nbk() -> int: + return 32 + + @staticmethod + def nbK() -> int: # No. of bytes requires to encode a public key + return 33 + + def __init__(self, coin_settings, network, swap_client=None): + super().__init__(network) + + def pkh(self, pubkey: bytes) -> bytes: + return ripemd160(blake256(pubkey)) + + def pkh_to_address(self, pkh: bytes) -> str: + prefix = self.chainparams_network()['pubkey_address'] + + data = prefix.to_bytes(2, 'big') + pkh + checksum = blake256(blake256(data)) + return b58encode(data + checksum[0:4]) + + def decode_address(self, address: str) -> bytes: + addr_data = b58decode(address) + if addr_data is None: + return None + prefixed_data = addr_data[:-4] + checksum = addr_data[-4:] + if blake256(blake256(prefixed_data))[:4] != checksum: + raise ValueError('Checksum mismatch') + return prefixed_data diff --git a/basicswap/util/address.py b/basicswap/util/address.py index 9dcfd7e..5deb589 100644 --- a/basicswap/util/address.py +++ b/basicswap/util/address.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2022-2023 tecnovert +# Copyright (c) 2022-2024 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. -import hashlib from basicswap.contrib.segwit_addr import bech32_decode, convertbits, bech32_encode -from basicswap.util.crypto import ripemd160 +from basicswap.util.crypto import ripemd160, sha256 __b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' @@ -68,7 +67,7 @@ def encodeStealthAddress(prefix_byte: int, scan_pubkey: bytes, spend_pubkey: byt data += bytes((0x00,)) # num prefix bits b = bytes((prefix_byte,)) + data - b += hashlib.sha256(hashlib.sha256(b).digest()).digest()[:4] + b += sha256(sha256(b))[:4] return b58encode(b) @@ -83,13 +82,12 @@ def toWIF(prefix_byte: int, b: bytes, compressed: bool = True) -> str: b = bytes((prefix_byte,)) + b if compressed: b += bytes((0x01,)) - b += hashlib.sha256(hashlib.sha256(b).digest()).digest()[:4] + b += sha256(sha256(b))[:4] return b58encode(b) def getKeyID(key_data: bytes) -> bytes: - sha256_hash = hashlib.sha256(key_data).digest() - return ripemd160(sha256_hash) + return ripemd160(sha256(key_data)) def bech32Decode(hrp, addr): @@ -109,18 +107,19 @@ def bech32Encode(hrp, data): return ret -def decodeAddress(address_str: str): - b58_addr = b58decode(address_str) - if b58_addr is not None: - address = b58_addr[:-4] - checksum = b58_addr[-4:] - assert (hashlib.sha256(hashlib.sha256(address).digest()).digest()[:4] == checksum), 'Checksum mismatch' - return b58_addr[:-4] - return None +def decodeAddress(address: str): + addr_data = b58decode(address) + if addr_data is None: + return None + prefixed_data = addr_data[:-4] + checksum = addr_data[-4:] + if sha256(sha256(prefixed_data))[:4] != checksum: + raise ValueError('Checksum mismatch') + return prefixed_data def encodeAddress(address: bytes) -> str: - checksum = hashlib.sha256(hashlib.sha256(address).digest()).digest() + checksum = sha256(sha256(address)) return b58encode(address + checksum[0:4]) diff --git a/basicswap/util/crypto.py b/basicswap/util/crypto.py index 22b80fc..4d0080c 100644 --- a/basicswap/util/crypto.py +++ b/basicswap/util/crypto.py @@ -1,23 +1,28 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2022-2023 tecnovert +# Copyright (c) 2022-2024 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. from Crypto.Hash import RIPEMD160, SHA256 # pycryptodome +from basicswap.contrib.blake256.blake256 import blake_hash -def sha256(data): +def sha256(data: bytes) -> bytes: h = SHA256.new() h.update(data) return h.digest() -def ripemd160(data): +def ripemd160(data: bytes) -> bytes: h = RIPEMD160.new() h.update(data) return h.digest() -def hash160(s): +def blake256(data: bytes) -> bytes: + return blake_hash(data) + + +def hash160(s: bytes) -> bytes: return ripemd160(sha256(s)) diff --git a/tests/basicswap/extended/test_dcr.py b/tests/basicswap/extended/test_dcr.py new file mode 100644 index 0000000..4be916c --- /dev/null +++ b/tests/basicswap/extended/test_dcr.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +import logging +import unittest + +from basicswap.basicswap import ( + Coins, +) +from tests.basicswap.util import ( + REQUIRED_SETTINGS, +) +from tests.basicswap.common import ( + stopDaemons, +) +from tests.basicswap.test_xmr import BaseTest +from basicswap.interface.dcr import DCRInterface + +logger = logging.getLogger() + + +class Test(BaseTest): + __test__ = True + test_coin_from = Coins.DCR + decred_daemons = [] + start_ltc_nodes = False + start_xmr_nodes = False + + @classmethod + def prepareExtraCoins(cls): + pass + + @classmethod + def tearDownClass(cls): + logging.info('Finalising Decred Test') + super(Test, cls).tearDownClass() + + stopDaemons(cls.decred_daemons) + + @classmethod + def coins_loop(cls): + super(Test, cls).coins_loop() + + def test_001_decred(self): + logging.info('---------- Test {}'.format(self.test_coin_from.name)) + + coin_settings = {'rpcport': 0, 'rpcauth': 'none'} + coin_settings.update(REQUIRED_SETTINGS) + + ci = DCRInterface(coin_settings, 'mainnet') + + k = ci.getNewSecretKey() + K = ci.getPubkey(k) + + pkh = ci.pkh(K) + address = ci.pkh_to_address(pkh) + assert (address.startswith('Ds')) + + data = ci.decode_address(address) + assert (data[2:] == pkh) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/basicswap/test_other.py b/tests/basicswap/test_other.py index 98adce3..3ca527e 100644 --- a/tests/basicswap/test_other.py +++ b/tests/basicswap/test_other.py @@ -24,12 +24,13 @@ from coincurve.keys import ( from basicswap.util import i2b, h2b from basicswap.util.integer import encode_varint, decode_varint -from basicswap.util.crypto import ripemd160, hash160 +from basicswap.util.crypto import ripemd160, hash160, blake256 from basicswap.util.network import is_private_ip_address from basicswap.util.rfc2440 import rfc2440_hash_password from basicswap.util_xmr import encode_address as xmr_encode_address from basicswap.interface.btc import BTCInterface from basicswap.interface.xmr import XMRInterface +from tests.basicswap.util import REQUIRED_SETTINGS from basicswap.basicswap_util import ( TxLockTypes) @@ -48,7 +49,6 @@ from basicswap.contrib.test_framework.script import hash160 as hash160_btc class Test(unittest.TestCase): - REQUIRED_SETTINGS = {'blocks_confirmed': 1, 'conf_target': 1, 'use_segwit': True, 'connection_type': 'rpc'} def test_serialise_num(self): def test_case(v, nb=None): @@ -69,7 +69,7 @@ class Test(unittest.TestCase): def test_sequence(self): coin_settings = {'rpcport': 0, 'rpcauth': 'none'} - coin_settings.update(self.REQUIRED_SETTINGS) + coin_settings.update(REQUIRED_SETTINGS) ci = BTCInterface(coin_settings, 'regtest') @@ -177,7 +177,7 @@ class Test(unittest.TestCase): def test_ecdsa_otves(self): coin_settings = {'rpcport': 0, 'rpcauth': 'none'} - coin_settings.update(self.REQUIRED_SETTINGS) + coin_settings.update(REQUIRED_SETTINGS) ci = BTCInterface(coin_settings, 'regtest') vk_sign = ci.getNewSecretKey() vk_encrypt = ci.getNewSecretKey() @@ -200,7 +200,7 @@ class Test(unittest.TestCase): def test_sign(self): coin_settings = {'rpcport': 0, 'rpcauth': 'none'} - coin_settings.update(self.REQUIRED_SETTINGS) + coin_settings.update(REQUIRED_SETTINGS) ci = BTCInterface(coin_settings, 'regtest') vk = ci.getNewSecretKey() @@ -215,7 +215,7 @@ class Test(unittest.TestCase): def test_sign_compact(self): coin_settings = {'rpcport': 0, 'rpcauth': 'none'} - coin_settings.update(self.REQUIRED_SETTINGS) + coin_settings.update(REQUIRED_SETTINGS) ci = BTCInterface(coin_settings, 'regtest') vk = ci.getNewSecretKey() @@ -230,7 +230,7 @@ class Test(unittest.TestCase): def test_sign_recoverable(self): coin_settings = {'rpcport': 0, 'rpcauth': 'none'} - coin_settings.update(self.REQUIRED_SETTINGS) + coin_settings.update(REQUIRED_SETTINGS) ci = BTCInterface(coin_settings, 'regtest') vk = ci.getNewSecretKey() @@ -246,7 +246,7 @@ class Test(unittest.TestCase): def test_pubkey_to_address(self): coin_settings = {'rpcport': 0, 'rpcauth': 'none'} - coin_settings.update(self.REQUIRED_SETTINGS) + coin_settings.update(REQUIRED_SETTINGS) ci = BTCInterface(coin_settings, 'regtest') pk = h2b('02c26a344e7d21bcc6f291532679559f2fd234c881271ff98714855edc753763a6') addr = ci.pubkey_to_address(pk) @@ -254,7 +254,7 @@ class Test(unittest.TestCase): def test_dleag(self): coin_settings = {'rpcport': 0, 'walletrpcport': 0, 'walletrpcauth': 'none'} - coin_settings.update(self.REQUIRED_SETTINGS) + coin_settings.update(REQUIRED_SETTINGS) ci = XMRInterface(coin_settings, 'regtest') @@ -430,32 +430,36 @@ class Test(unittest.TestCase): assert (msg_buf_v2.time_valid == 0) def test_is_private_ip_address(self): - assert (is_private_ip_address('localhost')) - assert (is_private_ip_address('127.0.0.1')) - assert (is_private_ip_address('10.0.0.0')) - assert (is_private_ip_address('172.16.0.0')) - assert (is_private_ip_address('192.168.0.0')) - - assert (is_private_ip_address('20.87.245.0') is False) - assert (is_private_ip_address('particl.io') is False) + test_addresses = [ + ('localhost', True), + ('127.0.0.1', True), + ('10.0.0.0', True), + ('172.16.0.0', True), + ('192.168.0.0', True), + ('20.87.245.0', False), + ('particl.io', False), + ] + for addr, is_private in test_addresses: + assert (is_private_ip_address(addr) is is_private) def test_varint(self): - def test_case(i, expect_length): + test_vectors = [ + (0, 1), + (1, 1), + (127, 1), + (128, 2), + (253, 2), + (8321, 2), + (16383, 2), + (16384, 3), + (2097151, 3), + (2097152, 4), + ] + for i, expect_length in test_vectors: b = encode_varint(i) assert (len(b) == expect_length) assert (decode_varint(b) == i) - test_case(0, 1) - test_case(1, 1) - test_case(127, 1) - test_case(128, 2) - test_case(253, 2) - test_case(8321, 2) - test_case(16383, 2) - test_case(16384, 3) - test_case(2097151, 3) - test_case(2097152, 4) - def test_base58(self): kv = edu.get_secret() Kv = edu.encodepoint(edf.scalarmult_B(kv)) @@ -468,6 +472,14 @@ class Test(unittest.TestCase): addr = xmr_encode_address(Kv, Ks, 4146) assert (addr.startswith('Wo')) + def test_blake256(self): + test_vectors = [ + ('716f6e863f744b9ac22c97ec7b76ea5f5908bc5b2f67c61510bfc4751384ea7a', b''), + ('7576698ee9cad30173080678e5965916adbb11cb5245d386bf1ffda1cb26c9d7', b'The quick brown fox jumps over the lazy dog'), + ] + for expect_hash, data in test_vectors: + assert (blake256(data).hex() == expect_hash) + if __name__ == '__main__': unittest.main() diff --git a/tests/basicswap/util.py b/tests/basicswap/util.py index e8be7b0..1985516 100644 --- a/tests/basicswap/util.py +++ b/tests/basicswap/util.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2022 tecnovert +# Copyright (c) 2022-2024 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php. @@ -10,6 +10,9 @@ import urllib from urllib.request import urlopen +REQUIRED_SETTINGS = {'blocks_confirmed': 1, 'conf_target': 1, 'use_segwit': True, 'connection_type': 'rpc'} + + def make_boolean(s): return s.lower() in ['1', 'true']