Add Decred support (#550)
* Refactor reorg_hashes function * Add Decred support
This commit is contained in:
parent
898e2eea00
commit
0815ff8e24
@ -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('<i32s32s32sH6sHBBIIQIIII32sI').unpack_from
|
||||
TX_COUNT = 4629388
|
||||
TX_COUNT_HEIGHT = 260628
|
||||
TX_PER_BLOCK = 17
|
||||
REORG_LIMIT = 1000
|
||||
RPC_PORT = 9109
|
||||
|
||||
@classmethod
|
||||
def header_hash(cls, header):
|
||||
'''Given a header return the hash.'''
|
||||
return cls.HEADER_HASH(header)
|
||||
|
||||
@classmethod
|
||||
def block(cls, raw_block, height):
|
||||
'''Return a Block namedtuple given a raw block and its height.'''
|
||||
if height > 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"
|
||||
|
||||
@ -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('<I', 0x10000 | (version & 0xffff))
|
||||
prefix_tx = no_witness_header + self.binary[start+4:end_prefix]
|
||||
tx_hash = self.blake256(prefix_tx)
|
||||
else:
|
||||
tx_hash = None
|
||||
|
||||
return TxDcr(
|
||||
version,
|
||||
inputs,
|
||||
@ -518,4 +543,4 @@ class DeserializerDecred(Deserializer):
|
||||
locktime,
|
||||
expiry,
|
||||
witness
|
||||
), DeserializerDecred.blake256(no_witness_tx)
|
||||
), tx_hash, self.cursor - start
|
||||
|
||||
@ -263,6 +263,16 @@ class BlockProcessor(electrumx.server.db.DB):
|
||||
The hashes are returned in order of increasing height. Start
|
||||
is the height of the first hash, last of the last.
|
||||
'''
|
||||
start, count = self.calc_reorg_range(count)
|
||||
last = start + count - 1
|
||||
s = '' if count == 1 else 's'
|
||||
self.logger.info(f'chain was reorganised replacing {count:,d} '
|
||||
f'block{s} at heights {start:,d}-{last:,d}')
|
||||
|
||||
return start, last, self.fs_block_hashes(start, count)
|
||||
|
||||
async def calc_reorg_range(self, count):
|
||||
'''Calculate the reorg range'''
|
||||
|
||||
def diff_pos(hashes1, hashes2):
|
||||
'''Returns the index of the first difference in the hash lists.
|
||||
@ -291,12 +301,7 @@ class BlockProcessor(electrumx.server.db.DB):
|
||||
else:
|
||||
start = (self.height - count) + 1
|
||||
|
||||
last = start + count - 1
|
||||
s = '' if count == 1 else 's'
|
||||
self.logger.info(f'chain was reorganised replacing {count:,d} '
|
||||
f'block{s} at heights {start:,d}-{last:,d}')
|
||||
|
||||
return start, last, self.fs_block_hashes(start, count)
|
||||
return start, count
|
||||
|
||||
def flush_state(self, batch):
|
||||
'''Flush chain state to the batch.'''
|
||||
@ -826,3 +831,13 @@ class BlockProcessor(electrumx.server.db.DB):
|
||||
self.blocks_event.set()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class DecredBlockProcessor(BlockProcessor):
|
||||
async def calc_reorg_range(self, count):
|
||||
start, count = super().calc_reorg_range(count)
|
||||
if start > 0:
|
||||
# A reorg in Decred can invalidate the previous block
|
||||
start -= 1
|
||||
count += 1
|
||||
return start, count
|
||||
|
||||
@ -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)
|
||||
|
||||
15
tests/blocks/decred_mainnet_100.json
Normal file
15
tests/blocks/decred_mainnet_100.json
Normal file
@ -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"
|
||||
}
|
||||
21
tests/blocks/decred_mainnet_120000.json
Normal file
21
tests/blocks/decred_mainnet_120000.json
Normal file
File diff suppressed because one or more lines are too long
@ -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"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
60
tests/test_transactions.py
Normal file
60
tests/test_transactions.py
Normal file
@ -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)
|
||||
79
tests/transactions/decred_mainnet_9cde4a.json
Normal file
79
tests/transactions/decred_mainnet_9cde4a.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user