From 0815ff8e24bef6c53ec329465c6f0cdf2e494927 Mon Sep 17 00:00:00 2001 From: "John L. Jegutanis" Date: Thu, 2 Aug 2018 15:20:36 +0200 Subject: [PATCH] Add Decred support (#550) * Refactor reorg_hashes function * Add Decred support --- electrumx/lib/coins.py | 63 ++++++++++++- electrumx/lib/tx.py | 61 +++++++++---- electrumx/server/block_processor.py | 27 ++++-- electrumx/server/daemon.py | 90 ++++++++++++++++++- tests/blocks/decred_mainnet_100.json | 15 ++++ tests/blocks/decred_mainnet_120000.json | 21 +++++ tests/lib/test_addresses.py | 25 +++--- tests/test_transactions.py | 60 +++++++++++++ tests/transactions/decred_mainnet_9cde4a.json | 79 ++++++++++++++++ 9 files changed, 402 insertions(+), 39 deletions(-) create mode 100644 tests/blocks/decred_mainnet_100.json create mode 100644 tests/blocks/decred_mainnet_120000.json create mode 100644 tests/test_transactions.py create mode 100644 tests/transactions/decred_mainnet_9cde4a.json diff --git a/electrumx/lib/coins.py b/electrumx/lib/coins.py index fae7cb9..6e81974 100644 --- a/electrumx/lib/coins.py +++ b/electrumx/lib/coins.py @@ -43,7 +43,7 @@ from electrumx.lib.hash import Base58, hash160, double_sha256, hash_to_hex_str from electrumx.lib.hash import HASHX_LEN from electrumx.lib.script import ScriptPubKey, OpCodes import electrumx.lib.tx as lib_tx -from electrumx.server.block_processor import BlockProcessor +import electrumx.server.block_processor as block_proc import electrumx.server.daemon as daemon from electrumx.server.session import ElectrumX, DashElectrumX @@ -69,7 +69,7 @@ class Coin(object): SESSIONCLS = ElectrumX DESERIALIZER = lib_tx.Deserializer DAEMON = daemon.Daemon - BLOCK_PROCESSOR = BlockProcessor + BLOCK_PROCESSOR = block_proc.BlockProcessor MEMPOOL_HISTOGRAM_REFRESH_SECS = 500 XPUB_VERBYTES = bytes('????', 'utf-8') XPRV_VERBYTES = bytes('????', 'utf-8') @@ -1676,6 +1676,65 @@ class BitcoinAtom(Coin): return deserializer.read_header(height, cls.BASIC_HEADER_SIZE) +class Decred(Coin): + NAME = "Decred" + SHORTNAME = "DCR" + NET = "mainnet" + XPUB_VERBYTES = bytes.fromhex("02fda926") + XPRV_VERBYTES = bytes.fromhex("02fda4e8") + P2PKH_VERBYTE = bytes.fromhex("073f") + P2SH_VERBYTES = [bytes.fromhex("071a")] + WIF_BYTE = bytes.fromhex("230e") + GENESIS_HASH = ('298e5cc3d985bfe7f81dc135f360abe0' + '89edd4396b86d2de66b0cef42b21d980') + BASIC_HEADER_SIZE = 180 + HEADER_HASH = lib_tx.DeserializerDecred.blake256 + DESERIALIZER = lib_tx.DeserializerDecred + DAEMON = daemon.DecredDaemon + BLOCK_PROCESSOR = block_proc.DecredBlockProcessor + ENCODE_CHECK = partial(Base58.encode_check, + hash_fn=lib_tx.DeserializerDecred.blake256d) + DECODE_CHECK = partial(Base58.decode_check, + hash_fn=lib_tx.DeserializerDecred.blake256d) + HEADER_UNPACK = struct.Struct(' 0: + return super().block(raw_block, height) + else: + return Block(raw_block, cls.block_header(raw_block, height), []) + + @classmethod + def electrum_header(cls, header, height): + labels = ('version', 'prev_block_hash', 'merkle_root', 'stake_root', + 'vote_bits', 'final_state', 'voters', 'fresh_stake', + 'revocations', 'pool_size', 'bits', 'sbits', 'block_height', + 'size', 'timestamp', 'nonce', 'extra_data', 'stake_version') + values = cls.HEADER_UNPACK(header) + h = dict(zip(labels, values)) + + # Convert some values + assert h['block_height'] == height + h['prev_block_hash'] = hash_to_hex_str(h['prev_block_hash']) + h['merkle_root'] = hash_to_hex_str(h['merkle_root']) + h['stake_root'] = hash_to_hex_str(h['stake_root']) + h['final_state'] = h['final_state'].hex() + h['extra_data'] = h['extra_data'].hex() + return h + + class Axe(Dash): NAME = "Axe" SHORTNAME = "AXE" diff --git a/electrumx/lib/tx.py b/electrumx/lib/tx.py index 8c4077c..03627db 100644 --- a/electrumx/lib/tx.py +++ b/electrumx/lib/tx.py @@ -29,6 +29,7 @@ from collections import namedtuple +from struct import pack from electrumx.lib.hash import sha256, double_sha256, hash_to_hex_str from electrumx.lib.util import ( @@ -428,8 +429,6 @@ class TxInputDcr(namedtuple("TxInput", "prev_hash prev_idx tree sequence")): @cachedproperty def is_coinbase(self): - # The previous output of a coin base must have a max value index and a - # zero hash. return (self.prev_hash == TxInputDcr.ZERO and self.prev_idx == TxInputDcr.MINUS_1) @@ -440,13 +439,13 @@ class TxInputDcr(namedtuple("TxInput", "prev_hash prev_idx tree sequence")): class TxOutputDcr(namedtuple("TxOutput", "value version pk_script")): - '''Class representing a transaction output.''' + '''Class representing a Decred transaction output.''' pass class TxDcr(namedtuple("Tx", "version inputs outputs locktime expiry " "witness")): - '''Class representing transaction that has a time field.''' + '''Class representing a Decred transaction.''' @cachedproperty def is_coinbase(self): @@ -454,22 +453,38 @@ class TxDcr(namedtuple("Tx", "version inputs outputs locktime expiry " class DeserializerDecred(Deserializer): - @staticmethod def blake256(data): from blake256.blake256 import blake_hash return blake_hash(data) + @staticmethod + def blake256d(data): + from blake256.blake256 import blake_hash + return blake_hash(blake_hash(data)) + + def read_tx(self): + return self._read_tx_parts(produce_hash=False)[0] + + def read_tx_and_hash(self): + tx, tx_hash, vsize = self._read_tx_parts() + return tx, tx_hash + + def read_tx_and_vsize(self): + tx, tx_hash, vsize = self._read_tx_parts(produce_hash=False) + return tx, vsize + def read_tx_block(self): '''Returns a list of (deserialized_tx, tx_hash) pairs.''' - read_tx = self.read_tx - txs = [read_tx() for _ in range(self._read_varint())] - stxs = [read_tx() for _ in range(self._read_varint())] + read = self.read_tx_and_hash + txs = [read() for _ in range(self._read_varint())] + stxs = [read() for _ in range(self._read_varint())] return txs + stxs - def _read_inputs(self): - read_input = self._read_input - return [read_input() for i in range(self._read_varint())] + def read_tx_tree(self): + '''Returns a list of deserialized_tx without tx hashes.''' + read_tx = self.read_tx + return [read_tx() for _ in range(self._read_varint())] def _read_input(self): return TxInputDcr( @@ -479,10 +494,6 @@ class DeserializerDecred(Deserializer): self._read_le_uint32(), # sequence ) - def _read_outputs(self): - read_output = self._read_output - return [read_output() for _ in range(self._read_varint())] - def _read_output(self): return TxOutputDcr( self._read_le_int64(), # value @@ -502,15 +513,29 @@ class DeserializerDecred(Deserializer): script = self._read_varbytes() return value_in, block_height, block_index, script - def read_tx(self): + def _read_tx_parts(self, produce_hash=True): start = self.cursor version = self._read_le_int32() inputs = self._read_inputs() outputs = self._read_outputs() locktime = self._read_le_uint32() expiry = self._read_le_uint32() - no_witness_tx = b'\x01\x00\x01\x00' + self.binary[start+4:self.cursor] + end_prefix = self.cursor witness = self._read_witness(len(inputs)) + + # Drop the coinbase-like input from a vote tx as it creates problems + # with UTXOs lookups and mempool management + if inputs[0].is_coinbase and len(inputs) > 1: + inputs = inputs[1:] + + if produce_hash: + # TxSerializeNoWitness << 16 == 0x10000 + no_witness_header = pack(' 0: + # A reorg in Decred can invalidate the previous block + start -= 1 + count += 1 + return start, count diff --git a/electrumx/server/daemon.py b/electrumx/server/daemon.py index 67f4e61..be22e99 100644 --- a/electrumx/server/daemon.py +++ b/electrumx/server/daemon.py @@ -17,8 +17,10 @@ from time import strptime import aiohttp -from electrumx.lib.util import int_to_varint, hex_to_bytes, class_logger -from electrumx.lib.hash import hex_str_to_hash +from electrumx.lib.util import int_to_varint, hex_to_bytes, class_logger, \ + unpack_uint16_from +from electrumx.lib.hash import hex_str_to_hash, hash_to_hex_str +from electrumx.lib.tx import DeserializerDecred from aiorpcx import JSONRPC @@ -365,3 +367,87 @@ class LegacyRPCDaemon(Daemon): if isinstance(t, int): return t return timegm(strptime(t, "%Y-%m-%d %H:%M:%S %Z")) + + +class DecredDaemon(Daemon): + async def raw_blocks(self, hex_hashes): + '''Return the raw binary blocks with the given hex hashes.''' + + params_iterable = ((h, False) for h in hex_hashes) + blocks = await self._send_vector('getblock', params_iterable) + + raw_blocks = [] + valid_tx_tree = {} + for block in blocks: + # Convert to bytes from hex + raw_block = hex_to_bytes(block) + raw_blocks.append(raw_block) + # Check if previous block is valid + prev = self.prev_hex_hash(raw_block) + votebits = unpack_uint16_from(raw_block[100:102])[0] + valid_tx_tree[prev] = self.is_valid_tx_tree(votebits) + + processed_raw_blocks = [] + for hash, raw_block in zip(hex_hashes, raw_blocks): + if hash in valid_tx_tree: + is_valid = valid_tx_tree[hash] + else: + # Do something complicated to figure out if this block is valid + header = await self._send_single('getblockheader', (hash, )) + if 'nextblockhash' not in header: + raise DaemonError(f'Could not find next block for {hash}') + next_hash = header['nextblockhash'] + next_header = await self._send_single('getblockheader', + (next_hash, )) + is_valid = self.is_valid_tx_tree(next_header['votebits']) + + if is_valid: + processed_raw_blocks.append(raw_block) + else: + # If this block is invalid remove the normal transactions + self.logger.info(f'block {hash} is invalidated') + processed_raw_blocks.append(self.strip_tx_tree(raw_block)) + + return processed_raw_blocks + + @staticmethod + def prev_hex_hash(raw_block): + return hash_to_hex_str(raw_block[4:36]) + + @staticmethod + def is_valid_tx_tree(votebits): + # Check if previous block was invalidated. + return bool(votebits & (1 << 0) != 0) + + def strip_tx_tree(self, raw_block): + c = self.coin + assert issubclass(c.DESERIALIZER, DeserializerDecred) + d = c.DESERIALIZER(raw_block, start=c.BASIC_HEADER_SIZE) + d.read_tx_tree() # Skip normal transactions + # Create a fake block without any normal transactions + return raw_block[:c.BASIC_HEADER_SIZE] + b'\x00' + raw_block[d.cursor:] + + async def height(self): + height = await super().height() + if height > 0: + # Lie about the daemon height as the current tip can be invalidated + height -= 1 + self._height = height + return height + + async def mempool_hashes(self): + mempool = await super().mempool_hashes() + # Add current tip transactions to the 'fake' mempool. + real_height = await self._send_single('getblockcount') + tip_hash = await self._send_single('getblockhash', (real_height,)) + tip = await self.deserialised_block(tip_hash) + # Add normal transactions except coinbase + mempool += tip['tx'][1:] + # Add stake transactions if applicable + mempool += tip.get('stx', []) + return mempool + + def client_session(self): + # FIXME allow self signed certificates + connector = aiohttp.TCPConnector(verify_ssl=False) + return aiohttp.ClientSession(connector=connector) diff --git a/tests/blocks/decred_mainnet_100.json b/tests/blocks/decred_mainnet_100.json new file mode 100644 index 0000000..4f448ae --- /dev/null +++ b/tests/blocks/decred_mainnet_100.json @@ -0,0 +1,15 @@ +{ + "hash": "0000000000017dd91008ec7c0ea63749b81d9a5188d9efc8d2d8cc0bdcff4d2a", + "size": 382, + "height": 100, + "merkleroot": "5c49629cefa3d5eb640a3236f6e970386e0b0826a5d33d566de36aec534fa93d", + "stakeroot": "0000000000000000000000000000000000000000000000000000000000000000", + "tx": [ + "c813acfcad624ccf19e6240358b95cbbb1b728ee94557556dc373cceae1a7e4b" + ], + "time": 1454961067, + "nonce": 3396292691, + "bits": "1b01ffff", + "previousblockhash": "000000000000dcecdf2c1ae9bb3e2e3135e7765b1902938ff67e2be489ab8131", + "block": "010000003181ab89e42b7ef68f9302195b76e735312e3ebbe91a2cdfecdc0000000000003da94f53ec6ae36d563dd3a526080b6e3870e9f636320a64ebd5a3ef9c62495c000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000ffff011b00c2eb0b00000000640000007e010000abf1b85653506fca9885f1c26941ecf1010000000000000000000000000000000000000000000000000000000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff03fa1a981200000000000017a914f5916158e3e2c4551c1796708db8367207ed13bb8700000000000000000000266a2464000000000000000000000000000000000000000000000000000000733ea5b290c04d1fdea1906f0000000000001976a9145b98376242c78de2003e7940d7e44270c39b83eb88ac000000000000000001d8bc28820000000000000000ffffffff0800002f646372642f00" +} diff --git a/tests/blocks/decred_mainnet_120000.json b/tests/blocks/decred_mainnet_120000.json new file mode 100644 index 0000000..8893aed --- /dev/null +++ b/tests/blocks/decred_mainnet_120000.json @@ -0,0 +1,21 @@ +{ + "hash": "00000000000002a7be42456746ffde5f336547d3c42123dc77369777998d4957", + "size": 2920, + "height": 120000, + "merkleroot": "1acccc06792056999231021ed4b0ed2402cde4e37033d4d586ed999e81444722", + "stakeroot": "37ae7cd6e26bc9e2c36e88ab5a82c6de1ec17c0fe9af1ee868ba6f1aaf1952f6", + "tx": [ + "df8a60acb00eca6091e1af7ea27a5aa82a270956f865e3c6f0e021e0bfc60483", + "9cde4a9685fa9e38ccf4da4bda1c1a123b3f2ed2c418f6bc128d7f5fbbee413d", + "da5766b9d68f7b10382d9e7b2f08c45de34d8cf345d8da3987f25f1c942aee63", + "619bcf9fc2029bba21598aea1bdbd53728ef0995454bcc706cf2bdc887879ddd", + "b4a311a1a7a6afdfad3aaeb045cb5659f0a0868e64d5cd3e21b37c9aee72bf9a", + "270ae1c44d341d16948c450e7a5a58b58dce4cb22d7e20ac2a629d4aeca94a37", + "b28162bc912ab4d99d68f49438eb7d861961d375639f3e0d105537cd35b12587" + ], + "time": 1490849134, + "nonce": 2527962321, + "bits": "1a054c72", + "previousblockhash": "00000000000000d2a3f4e55c59c90915a5ed3435dfb2405101a1ac604328e7d0", + "block": "03000000d0e7284360aca1015140b2df3534eda51509c9595ce5f4a3d200000000000000224744819e99ed86d5d43370e3e4cd0224edb0d41e0231929956207906cccc1af65219af1a6fba68e81eafe90f7cc11edec6825aab886ec3e2c96be2d67cae3701003a5dc9b6b4c105000000c8a10000724c051a1c20703702000000c0d40100680b00006e8ddc58d1a4ad9690fe010000502e018100249d0000000000000000000000000000000000000000000000000201000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff031421640f00000000000017a914f5916158e3e2c4551c1796708db8367207ed13bb8700000000000000000000266a24c0d40100000000000000000000000000000000000000000000000000fb16d3c7d581d0629203635c0000000000001976a914d392618e1d9923f5f49fe6240760ee039eae388888ac0000000000000000018ce7bc6b0000000000000000ffffffff0800002f646372642f01000000022b7a5d0b09429a430ee2d465ff4ec1cec8080f66c59989ef03680dcb26a817460400000000ffffffff4d1bb7488ea38c16542cb80f505cc3d0d1757e3887ba25c8256619ec9ab1c5c00000000000ffffffff09eb3334030000000000001976a914ea9ab497c58159b8b0bf23becd179c076f8fbb9188ac78d0f4020000000000001976a9144820daabe97c3efcd3b34dfa51caf432d68d6a4d88ac541965340000000000001976a91495880e8c485ee28349d9885aa0fe448aa96e237488acb42f94000000000000001976a914a0d1579a51dbb6f26469944711d7e3f2f7a0b26088ac2a01fa020000000000001976a91444386bacdf3d7a353dabb2b334db9506791e83f288ac5c3be50b0000000000001976a9149f66566103e93e3cc29bebfed48450c3869e9a7688acc6fc91000000000000001976a91492ebcb796c918f5db19540726f91ca06a3b7640a88acd2b1c23c0000000000001976a9148806f0ae0b862006e39835d8cf91ac7f9d1c97d588ac6790f5020000000000001976a91418f82896bef63e67e125951bc464362eefdb00bb88ac0000000000000000021ee5573600000000bfd40100010000006a473044022051876acd9f0b716eaf383873db604cd7f39d659db2e909701dcbd655b469121e022036c165ab21c16865bab81696b41b3dcb85eb802867490b034f4cce2ac643b7640121026ea7725f46b9ab3931dd789907e80380eb91771cc681a1711c4468b71589d390ea20fe5300000000bad40100010000006a4730440220401007a7c4ba4b982babb08120b62f260484f61491e557f0a0d7d9c4484acf0e022041288a676750d421c63e09e7c11787a49bfec49e5490202220ddeb3406e2d3da01210371f6b9914081c10adfbac8fde9c9731c29fdb1bb235ba70f28cb0cf5f7268b2e0501000000020000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffffa4ce018b25f1294aa9fbde51d4c30357f53c7ca89c004c83c1abfa93b158ad2e0000000001ffffffff0400000000000000000000266a24d0e7284360aca1015140b2df3534eda51509c9595ce5f4a3d200000000000000bfd4010000000000000000000000086a06010002000000e5f073000000000000001abb76a91490ccfecf5a9a75b9076eb3c31f8138fc602206f988acc2bd193e0100000000001abb76a9149143f9b3523887569bd9235c3ac1712bbff7dc5f88ac000000000000000002d8133c090000000000000000ffffffff020000d09a5135010000005fce0100100000009047304402200fb8ddf9ac5bdc8f06bae46904ca0128c45456c016d5cd7c1da4f859af18ab3e0220065abecb13049681b33b7a7cc78e1bfcec89917caa7e3b19b76450731a73b2260147512103c1b6098bd7f6ab315a9cbc2d670793b12b835bc4931e21d5e4cdcf86c9bb034e210322000a0468f6aad07210cf7b344690cbe62421f5c48da471e7e26687438bcbb852ae01000000020000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff267ece00044f4284da13c91b4e9df9d26486140fb18dfadb75e7c707e9436d930000000001ffffffff0400000000000000000000266a24d0e7284360aca1015140b2df3534eda51509c9595ce5f4a3d200000000000000bfd4010000000000000000000000086a060100030000006eef73000000000000001abb76a91475449c1e1720e2005d51784a13d8c53651ecadb288ace7bd395c0100000000001abb76a91452bb6ef88c048a68d2ccdfcfdffe5f6e6029b5a488ac000000000000000002d8133c090000000000000000ffffffff0200007e99715301000000e2cc01000b0000009047304402206eff64f438e999b4178030243435b32050768b29fc49d3188acecba6d1439d7e0220160bd0bfda07a07048b9073c6a175cab0f5f7c118afac2ba1ae1673ad2023efc0147512103a75b0d7b58a3c193621a414646688bd2dbccfd0079729d2007c286225778868d21034f79ac3d651f6fd79daa4526b9e0cdc57d3d8495e3f0c4fc2d9b2c899e29d83852ae01000000020000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff55fc29cc05253b5765a1e93ef1677711f5b9f9fba4e38c99e004da8066f8c3400000000001ffffffff0300000000000000000000266a24d0e7284360aca1015140b2df3534eda51509c9595ce5f4a3d200000000000000bfd4010000000000000000000000086a0601000300000047873ad90000000000001abb76a91459df4a46149b695b6b9cabc965022e9b7f0fd27088ac000000000000000002d8133c090000000000000000ffffffff0200006f73fecf00000000949d0100180000006b483045022100c9cda6ade9823c54347ebd4a8aee0fbaa343fdd1d7583aece4b986aa5ce0e33502205d8b6ce189095199b54ba0566aecb5e7728cda07d1dd53c37cdc10cb0c45cfb001210225e54c53ee31686b37630ff9275b6f6a04c5774800740e8d6d976f6f003741fa01000000020000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff2ef74fe9f72a5c206be9c106059d47f2c0620f7dcffd84f03c6bc1988ca07e350000000001ffffffff0300000000000000000000266a24d0e7284360aca1015140b2df3534eda51509c9595ce5f4a3d200000000000000bfd4010000000000000000000000086a06010003000000a64d144d0100000000001abb76a91470ba09770582a7ecb86f4c9c20e3e8692bfaae2288ac000000000000000002d8133c090000000000000000ffffffff020000ce39d843010000005bc601000d0000006a47304402201fd1789db732ddc110ea8a4419ae39d05a4be4a372d0f72ec907d2ef84bd28f7022002b617f2d5963331346177374db4fc7d45833cc4ad3eaba34737830c9d7e73e1012102cdedefa4a977300348df4edb246c1246a354f17d5a35fbd57c655bd29f8729b701000000020000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff2d5d7fbbf76d556c306725127a8aa377f0de99d68f164b1bdd4848dfac7008250000000001ffffffff0300000000000000000000266a24d0e7284360aca1015140b2df3534eda51509c9595ce5f4a3d200000000000000bfd4010000000000000000000000086a060100030000005a87834c0100000000001abb76a91494173c568bf08a52cb382796f8214691bfd44f5788ac000000000000000002d8133c090000000000000000ffffffff0200008273474301000000f8bc01000d0000006a47304402204d63fc11c54a8dade738bc9e93ed1a46319316540438aa6eccf6799280fbe17b022047a2d97fb4b0cbbae0de329edda09221edad3db306668ea500a0b4cdea5e3f34012102e5b3099c3eda57962fd30b51647d76377f4470b2f647cb165be2626d46a25821" +} \ No newline at end of file diff --git a/tests/lib/test_addresses.py b/tests/lib/test_addresses.py index b59edec..c4c02f6 100644 --- a/tests/lib/test_addresses.py +++ b/tests/lib/test_addresses.py @@ -26,28 +26,31 @@ import pytest -from electrumx.lib.coins import Litecoin, BitcoinCash, Zcash, Emercoin, BitcoinGold -from electrumx.lib.hash import Base58 +import electrumx.lib.coins as coins addresses = [ - (BitcoinCash, "13xDKJbjh4acmLpNVr6Lc9hFcXRr9fyt4x", + (coins.BitcoinCash, "13xDKJbjh4acmLpNVr6Lc9hFcXRr9fyt4x", "206168f5322583ff37f8e55665a4789ae8963532", "b8cb80b26e8932f5b12a7e"), - (BitcoinCash, "3GxRZWkJufR5XA8hnNJgQ2gkASSheoBcmW", + (coins.BitcoinCash, "3GxRZWkJufR5XA8hnNJgQ2gkASSheoBcmW", "a773db925b09add367dcc253c1f9bbc1d11ec6fd", "062d8515e50cb92b8a3a73"), - (BitcoinGold, "GZjH8pETu5xXd5DTt5VAqS9giooLNoHjnJ", + (coins.BitcoinGold, "GZjH8pETu5xXd5DTt5VAqS9giooLNoHjnJ", "ae40655d7006806fd668248d10e7822c0b774dab", "3a1af301b378ad92493b17"), - (BitcoinGold, "AXfENBm9FP1PMa8AWnVPZZ4tHEwBiqNZav", + (coins.BitcoinGold, "AXfENBm9FP1PMa8AWnVPZZ4tHEwBiqNZav", "ae40655d7006806fd668248d10e7822c0b774dab", "cb3db4271432c0ac9f88d5"), - (Emercoin, "ELAeVHQg2mmdTTrTrZSzMgAQyXfC9TSRys", + (coins.Emercoin, "ELAeVHQg2mmdTTrTrZSzMgAQyXfC9TSRys", "210c4482ad8eacb0d349992973608300677adb15", "d71f2df4ef1b397088d731"), - (Litecoin, "LNBAaWuZmipg29WXfz5dtAm1pjo8FEH8yg", + (coins.Litecoin, "LNBAaWuZmipg29WXfz5dtAm1pjo8FEH8yg", "206168f5322583ff37f8e55665a4789ae8963532", "b8cb80b26e8932f5b12a7e"), - (Litecoin, "MPAZsQAGrnGWKfQbtFJ2Dfw9V939e7D3E2", + (coins.Litecoin, "MPAZsQAGrnGWKfQbtFJ2Dfw9V939e7D3E2", "a773db925b09add367dcc253c1f9bbc1d11ec6fd", "062d8515e50cb92b8a3a73"), - (Zcash, "t1LppKe1sfPNDMysGSGuTjxoAsBcvvSYv5j", + (coins.Zcash, "t1LppKe1sfPNDMysGSGuTjxoAsBcvvSYv5j", "206168f5322583ff37f8e55665a4789ae8963532", "b8cb80b26e8932f5b12a7e"), - (Zcash, "t3Zq2ZrASszCg7oBbio7oXqnfR6dnSWqo76", + (coins.Zcash, "t3Zq2ZrASszCg7oBbio7oXqnfR6dnSWqo76", "a773db925b09add367dcc253c1f9bbc1d11ec6fd", "062d8515e50cb92b8a3a73"), + (coins.Decred, "DsUZxxoHJSty8DCfwfartwTYbuhmVct7tJu", + "2789d58cfa0957d206f025c2af056fc8a77cebb0", "8cc9b11122272bd7b79a50"), + (coins.Decred, "DcuQKx8BES9wU7C6Q5VmLBjw436r27hayjS", + "f0b4e85100aee1a996f22915eb3c3f764d53779a", "a03c1a27de9ac3b3122e8d"), ] diff --git a/tests/test_transactions.py b/tests/test_transactions.py new file mode 100644 index 0000000..747c7fb --- /dev/null +++ b/tests/test_transactions.py @@ -0,0 +1,60 @@ +# Copyright (c) 2018, John L. Jegutanis +# +# All rights reserved. +# +# See the file "LICENCE" for information about the copyright +# and warranty status of this software. + +import json +import os +from binascii import unhexlify + +import pytest + +from electrumx.lib.coins import Coin +from electrumx.lib.hash import hash_to_hex_str + +TRANSACTION_DIR = os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'transactions') + +# Find out which db engines to test +# Those that are not installed will be skipped +transactions = [] + +for name in os.listdir(TRANSACTION_DIR): + try: + name_parts = name.split("_") + coinFound = Coin.lookup_coin_class(name_parts[0], name_parts[1]) + with open(os.path.join(TRANSACTION_DIR, name)) as f: + transactions.append((coinFound, json.load(f))) + except Exception as e: + transactions.append(pytest.fail(name)) + + +@pytest.fixture(params=transactions) +def transaction_details(request): + return request.param + + +def test_transaction(transaction_details): + coin, tx_info = transaction_details + + raw_tx = unhexlify(tx_info['hex']) + tx, tx_hash = coin.DESERIALIZER(raw_tx, 0).read_tx_and_hash() + assert tx_info['txid'] == hash_to_hex_str(tx_hash) + + vin = tx_info['vin'] + for i in range(len(vin)): + assert vin[i]['txid'] == hash_to_hex_str(tx.inputs[i].prev_hash) + assert vin[i]['vout'] == tx.inputs[i].prev_idx + + vout = tx_info['vout'] + for i in range(len(vout)): + # value pk_script + assert vout[i]['value'] == tx.outputs[i].value + spk = vout[i]['scriptPubKey'] + tx_pks = tx.outputs[i].pk_script + assert spk['hex'] == tx_pks.hex() + assert spk['address'] == coin.address_from_script(tx_pks) + assert coin.address_to_hashX(spk['address']) == \ + coin.hashX_from_script(tx_pks) diff --git a/tests/transactions/decred_mainnet_9cde4a.json b/tests/transactions/decred_mainnet_9cde4a.json new file mode 100644 index 0000000..afb4716 --- /dev/null +++ b/tests/transactions/decred_mainnet_9cde4a.json @@ -0,0 +1,79 @@ +{ + "hex": "01000000022b7a5d0b09429a430ee2d465ff4ec1cec8080f66c59989ef03680dcb26a817460400000000ffffffff4d1bb7488ea38c16542cb80f505cc3d0d1757e3887ba25c8256619ec9ab1c5c00000000000ffffffff09eb3334030000000000001976a914ea9ab497c58159b8b0bf23becd179c076f8fbb9188ac78d0f4020000000000001976a9144820daabe97c3efcd3b34dfa51caf432d68d6a4d88ac541965340000000000001976a91495880e8c485ee28349d9885aa0fe448aa96e237488acb42f94000000000000001976a914a0d1579a51dbb6f26469944711d7e3f2f7a0b26088ac2a01fa020000000000001976a91444386bacdf3d7a353dabb2b334db9506791e83f288ac5c3be50b0000000000001976a9149f66566103e93e3cc29bebfed48450c3869e9a7688acc6fc91000000000000001976a91492ebcb796c918f5db19540726f91ca06a3b7640a88acd2b1c23c0000000000001976a9148806f0ae0b862006e39835d8cf91ac7f9d1c97d588ac6790f5020000000000001976a91418f82896bef63e67e125951bc464362eefdb00bb88ac0000000000000000021ee5573600000000bfd40100010000006a473044022051876acd9f0b716eaf383873db604cd7f39d659db2e909701dcbd655b469121e022036c165ab21c16865bab81696b41b3dcb85eb802867490b034f4cce2ac643b7640121026ea7725f46b9ab3931dd789907e80380eb91771cc681a1711c4468b71589d390ea20fe5300000000bad40100010000006a4730440220401007a7c4ba4b982babb08120b62f260484f61491e557f0a0d7d9c4484acf0e022041288a676750d421c63e09e7c11787a49bfec49e5490202220ddeb3406e2d3da01210371f6b9914081c10adfbac8fde9c9731c29fdb1bb235ba70f28cb0cf5f7268b2e", + "txid": "9cde4a9685fa9e38ccf4da4bda1c1a123b3f2ed2c418f6bc128d7f5fbbee413d", + "vin": [ + { + "txid": "4617a826cb0d6803ef8999c5660f08c8cec14eff65d4e20e439a42090b5d7a2b", + "vout": 4 + }, + { + "txid": "c0c5b19aec196625c825ba87387e75d1d0c35c500fb82c54168ca38e48b71b4d", + "vout": 0 + } + ], + "vout": [ + { + "value": 53752811, + "scriptPubKey": { + "hex": "76a914ea9ab497c58159b8b0bf23becd179c076f8fbb9188ac", + "address": "DsnMNwSYjwgks3c1kWVbaB6npxv8LXnGc8U" + } + }, + { + "value": 49598584, + "scriptPubKey": { + "hex": "76a9144820daabe97c3efcd3b34dfa51caf432d68d6a4d88ac", + "address": "DsXYHVzSPfo4jrYAkLJN7yFuAfY3mxA4JGy" + } + }, + { + "value": 879040852, + "scriptPubKey": { + "hex": "76a91495880e8c485ee28349d9885aa0fe448aa96e237488ac", + "address": "DsebZAQadwBurUCowGbsbgYTpMETHBibZLf" + } + }, + { + "value": 9711540, + "scriptPubKey": { + "hex": "76a914a0d1579a51dbb6f26469944711d7e3f2f7a0b26088ac", + "address": "DsfdEPUTpsCBJjjS8ovgp65uYwws18FQpas" + } + }, + { + "value": 49938730, + "scriptPubKey": { + "hex": "76a91444386bacdf3d7a353dabb2b334db9506791e83f288ac", + "address": "DsXBd2eWDHujo7wjYTsUwRw7wHoiYN5Ynhg" + } + }, + { + "value": 199572316, + "scriptPubKey": { + "hex": "76a9149f66566103e93e3cc29bebfed48450c3869e9a7688ac", + "address": "DsfVjXTdcisQ6j5c5ZrhLFfJC56ZMCCwkr2" + } + }, + { + "value": 9567430, + "scriptPubKey": { + "hex": "76a91492ebcb796c918f5db19540726f91ca06a3b7640a88ac", + "address": "DseMkckQQVxjEu6t2bTD65gW4EY2GiGuhNk" + } + }, + { + "value": 1019392466, + "scriptPubKey": { + "hex": "76a9148806f0ae0b862006e39835d8cf91ac7f9d1c97d588ac", + "address": "DsdN9hjHeY8FiDYdEpesfTy215sdN5Ly74v" + } + }, + { + "value": 49647719, + "scriptPubKey": { + "hex": "76a91418f82896bef63e67e125951bc464362eefdb00bb88ac", + "address": "DsTEvzPjXppZGYkLXZdZN16FwGaeMEjudpe" + } + } + ] +}