diff --git a/src/electrumx/lib/tx.py b/src/electrumx/lib/tx.py index 51c7536..22a2938 100644 --- a/src/electrumx/lib/tx.py +++ b/src/electrumx/lib/tx.py @@ -28,16 +28,14 @@ '''Transaction-related classes and functions.''' from dataclasses import dataclass -from hashlib import blake2s from typing import Sequence, Optional, Tuple -from electrumx.lib.hash import sha256, double_sha256, hash_to_hex_str -from electrumx.lib.script import OpCodes +from electrumx.lib.hash import double_sha256, hash_to_hex_str from electrumx.lib.util import ( unpack_le_int32_from, unpack_le_int64_from, unpack_le_uint16_from, unpack_be_uint16_from, unpack_le_uint32_from, unpack_le_uint64_from, pack_le_int32, pack_varint, - pack_le_uint16, pack_le_uint32, pack_le_int64, pack_varbytes, + pack_le_uint32, pack_le_int64, pack_varbytes, ) ZERO = bytes(32) @@ -323,1138 +321,3 @@ class DeserializerSegWit(Deserializer): def read_tx(self): return self._read_tx_parts()[0] - - -###################################################################### -# Non-Bitcoin stuff goes strictly below this line. -###################################################################### - - -class DeserializerLitecoin(DeserializerSegWit): - '''Class representing Litecoin transactions, which may have the MW flag set. - - This handles regular and segwit Litecoin transactions, with special handling - for a limited set of MW transactions. All transactions in a block are parsed - correctly without error, however certain MW transactions in mempool cannot - be parsed and will raise a SkipTxDeserialize exception. - - When litecoind is run with the latest/default RPC serialization version (2), - a SkipTxDeserialize exception will be raised for any transaction that has - the MW flag bit set (0x8) AND has a non-null mwtx section. - - When litecoind is run with -rpcserialversion=1, only pure MW-only - transactions that have no regular inputs or outputs will raise this - exception. This is a workaround for a bug in v0.21.2.1 that lists these - transactions in the getrawmempool result even though when served with the MW - data stripped they are invalid. Presently, such transactions are of no - interest to electrumx, and they vanish into the MWEB when mined. - ''' - def _read_tx_parts(self): - orig_start = self.cursor - start = self.cursor - marker = self.binary[self.cursor + 4] - if marker: # non-segwit - tx = Deserializer.read_tx(self) - return tx, self.cursor - orig_start - - version = self._read_le_int32() - orig_ser = self.binary[start:self.cursor] - - marker = self._read_byte() - flag = self._read_byte() - - # Work around a bug in litecoind v0.21.2 with -rpcserialversion=1 that - # returns invalid stripped MW-only transactions with no regular inputs - # or outputs, causing this to look like a segwit transaction, but it - # really just has zeros for the number of inputs and outputs, which we - # incorrectly interpreted as a marker and flag. If what we parsed as - # marker and flag are both zero, this is such an invalid transaction. - if flag == 0: - raise SkipTxDeserialize('invalid MW-only transaction with no regular inputs or outputs') - - start = self.cursor - inputs = self._read_inputs() - outputs = self._read_outputs() - orig_ser += self.binary[start:self.cursor] - - # https://github.com/litecoin-project/litecoin/blob/948e6257aec15b52ef68b4e1ee9d73f7c740fae3/src/primitives/transaction.h#L299 - witness_start = self.cursor - if flag & 1: # witness flag - witness = self._read_witness(len(inputs)) - else: - # A MW tx is allowed to not have the witness bit of the flag byte - # set, indicating no witness data encoded for the inputs. Perhaps we - # should return a normal Tx here instead, but there is a flag byte - # we should probably not just discard. - witness = [] - witness_size = self.cursor - witness_start + 2 # +2 due to marker and flag bytes - - if flag & 8: # MWEB flag - # If this transaction is in the main block, not the MW extension - # block (MWEB), this should be the HogEx/integration transaction, - # which has no mweb tx, just a single zero byte. - # https://github.com/litecoin-project/litecoin/blob/bb242e33551157e0db3b70f90f5738f34b82cc51/src/mweb/mweb_node.cpp#L17-L20 - # - # If this is anything but a zero byte, we cannot (yet) deserialize - # this transaction since the mwtx at this location is variable - # length and we cannot determine the locktime that follows without - # accurately parsing it. - # - # Only in mempool should we encounter transactions with mwtx data, - # so for the time being we will simply ignore these transactions. - # When such transactions are mined, they will either have the MW - # data stripped when they enter the regular block, or if they have - # no regular inputs or outputs (MW-only) they will only be in the - # MWEB, not the regular block at all. - # - # Note that by running litecoind with -rpcserialversion=1, the MW - # data will be stripped from transactions and this tx flag will be - # cleared. - if self._read_byte() != 0: - raise SkipTxDeserialize('non-null mwtx bytes are not parseable') - - start = self.cursor - locktime = self._read_le_uint32() - orig_ser += self.binary[start:self.cursor] - base_size = self.cursor - orig_start - witness_size - weight = 4 * base_size + witness_size - vsize = weight // 4 + (weight % 4 > 0) - - txid = self.TX_HASH_FN(orig_ser) - wtxid = self.TX_HASH_FN(self.binary[orig_start:self.cursor]) - - return TxSegWit( - version=version, - marker=marker, - flag=flag, - inputs=inputs, - outputs=outputs, - witness=witness, - locktime=locktime, - txid_rev=txid, - wtxid_rev=wtxid), vsize - - def read_tx(self): - return self._read_tx_parts()[0] - - -class DeserializerAuxPow(Deserializer): - VERSION_AUXPOW = (1 << 8) - - def read_auxpow(self): - '''Reads and returns the CAuxPow data''' - - # We first calculate the size of the CAuxPow instance and then - # read it as bytes in the final step. - start = self.cursor - - self.read_tx() # AuxPow transaction - self.cursor += 32 # Parent block hash - merkle_size = self._read_varint() - self.cursor += 32 * merkle_size # Merkle branch - self.cursor += 4 # Index - merkle_size = self._read_varint() - self.cursor += 32 * merkle_size # Chain merkle branch - self.cursor += 4 # Chain index - self.cursor += 80 # Parent block header - - end = self.cursor - self.cursor = start - return self._read_nbytes(end - start) - - def read_header(self, static_header_size): - '''Return the AuxPow block header bytes''' - - # We are going to calculate the block size then read it as bytes - start = self.cursor - - version = self._read_le_uint32() - if version & self.VERSION_AUXPOW: - self.cursor = start - self.cursor += static_header_size # Block normal header - self.read_auxpow() - header_end = self.cursor - else: - header_end = start + static_header_size - - self.cursor = start - return self._read_nbytes(header_end - start) - - -class DeserializerAuxPowSegWit(DeserializerSegWit, DeserializerAuxPow): - pass - - -class DeserializerEquihash(Deserializer): - def read_header(self, static_header_size): - '''Return the block header bytes''' - start = self.cursor - # We are going to calculate the block size then read it as bytes - self.cursor += static_header_size - solution_size = self._read_varint() - self.cursor += solution_size - header_end = self.cursor - self.cursor = start - return self._read_nbytes(header_end) - - -class DeserializerEquihashSegWit(DeserializerSegWit, DeserializerEquihash): - pass - - -class DeserializerZcash(DeserializerEquihash): - def read_tx(self): - orig_start = self.cursor - start = self.cursor - header = self._read_le_uint32() - overwintered = ((header >> 31) == 1) - if overwintered: - version = header & 0x7fffffff - self.cursor += 4 # versionGroupId - else: - version = header - - is_overwinter_v3 = version == 3 - is_sapling_v4 = version == 4 - - base_tx = Tx( - version=version, - inputs=self._read_inputs(), - outputs=self._read_outputs(), - locktime=self._read_le_uint32(), - txid_rev=None, - wtxid_rev=None, - ) - - if is_overwinter_v3 or is_sapling_v4: - self.cursor += 4 # expiryHeight - - has_shielded = False - if is_sapling_v4: - self.cursor += 8 # valueBalance - shielded_spend_size = self._read_varint() - self.cursor += shielded_spend_size * 384 # vShieldedSpend - shielded_output_size = self._read_varint() - self.cursor += shielded_output_size * 948 # vShieldedOutput - has_shielded = shielded_spend_size > 0 or shielded_output_size > 0 - - if base_tx.version >= 2: - joinsplit_size = self._read_varint() - if joinsplit_size > 0: - joinsplit_desc_len = 1506 + (192 if is_sapling_v4 else 296) - # JSDescription - self.cursor += joinsplit_size * joinsplit_desc_len - self.cursor += 32 # joinSplitPubKey - self.cursor += 64 # joinSplitSig - - if is_sapling_v4 and has_shielded: - self.cursor += 64 # bindingSig - - base_tx.txid_rev = base_tx.wtxid_rev = self.TX_HASH_FN(self.binary[orig_start:self.cursor]) - return base_tx - - -@dataclass(kw_only=True, slots=True) -class TxPIVX(Tx): - '''Class representing a PIVX transaction.''' - txtype: int - - def serialize(self): - return b''.join(( - pack_le_uint16(self.version), - pack_le_uint16(self.txtype), - pack_varint(len(self.inputs)), - b''.join(tx_in.serialize() for tx_in in self.inputs), - pack_varint(len(self.outputs)), - b''.join(tx_out.serialize() for tx_out in self.outputs), - pack_le_uint32(self.locktime) - )) - - -class DeserializerPIVX(Deserializer): - def read_tx(self): - orig_start = self.cursor - start = self.cursor - header = self._read_le_uint32() - tx_type = header >> 16 # DIP2 tx type - if tx_type: - version = header & 0x0000ffff - else: - version = header - - if tx_type and version < 3: - version = header - tx_type = 0 - - base_tx = TxPIVX( - version=version, - txtype=tx_type, - inputs=self._read_inputs(), - outputs=self._read_outputs(), - locktime=self._read_le_uint32(), - txid_rev=None, - wtxid_rev=None, - ) - - if version >= 3: # >= sapling - self._read_varint() - self.cursor += 8 # valueBalance - shielded_spend_size = self._read_varint() - self.cursor += shielded_spend_size * 384 # vShieldedSpend - shielded_output_size = self._read_varint() - self.cursor += shielded_output_size * 948 # vShieldedOutput - self.cursor += 64 # bindingSig - if (tx_type > 0): - self.cursor += 2 # extraPayload - - base_tx.txid_rev = base_tx.wtxid_rev = self.TX_HASH_FN(self.binary[orig_start:self.cursor]) - return base_tx - - -@dataclass(kw_only=True, slots=True) -class TxTime(Tx): - '''Class representing transaction that has a time field.''' - time: int - - -class DeserializerTxTime(Deserializer): - def read_tx(self): - orig_start = self.cursor - start = self.cursor - tx = TxTime( - version=self._read_le_int32(), - time=self._read_le_uint32(), - inputs=self._read_inputs(), - outputs=self._read_outputs(), - locktime=self._read_le_uint32(), - txid_rev=None, - wtxid_rev=None, - ) - tx.txid_rev = tx.wtxid_rev = self.TX_HASH_FN(self.binary[orig_start:self.cursor]) - return tx - - -@dataclass(kw_only=True, slots=True) -class TxTimeSegWit(TxSegWit): - '''Class representing a SegWit transaction with time.''' - time: int - - -class DeserializerTxTimeSegWit(DeserializerTxTime): - def _read_witness(self, fields): - read_witness_field = self._read_witness_field - return [read_witness_field() for _ in range(fields)] - - def _read_witness_field(self): - read_varbytes = self._read_varbytes - return [read_varbytes() for _ in range(self._read_varint())] - - def _read_tx_parts(self): - orig_start = self.cursor - start = self.cursor - marker = self.binary[self.cursor + 8] - if marker: # non-segwit - tx = DeserializerTxTime.read_tx(self) - return tx, self.cursor - orig_start - - version = self._read_le_int32() - time = self._read_le_uint32() - orig_ser = self.binary[start:self.cursor] - - marker = self._read_byte() - flag = self._read_byte() - - start = self.cursor - inputs = self._read_inputs() - outputs = self._read_outputs() - orig_ser += self.binary[start:self.cursor] - - witness_start = self.cursor - witness = self._read_witness(len(inputs)) - witness_size = self.cursor - witness_start + 2 # +2 due to marker and flag bytes - - start = self.cursor - locktime = self._read_le_uint32() - orig_ser += self.binary[start:self.cursor] - base_size = self.cursor - orig_start - witness_size - weight = 4 * base_size + witness_size - vsize = weight // 4 + (weight % 4 > 0) - - txid = self.TX_HASH_FN(orig_ser) - wtxid = self.TX_HASH_FN(self.binary[orig_start:self.cursor]) - - tx = TxTimeSegWit( - version=version, - time=time, - marker=marker, - flag=flag, - inputs=inputs, - outputs=outputs, - witness=witness, - locktime=locktime, - txid_rev=txid, - wtxid_rev=wtxid, - ) - return tx, vsize - - def read_tx(self): - return self._read_tx_parts()[0] - - -class DeserializerTxTimeSegWitNavCoin(DeserializerTxTime): - def _read_witness(self, fields): - read_witness_field = self._read_witness_field - return [read_witness_field() for _ in range(fields)] - - def _read_witness_field(self): - read_varbytes = self._read_varbytes - return [read_varbytes() for _ in range(self._read_varint())] - - def read_tx_no_segwit(self): - start = self.cursor - version = self._read_le_int32() - time = self._read_le_uint32() - inputs = self._read_inputs() - outputs = self._read_outputs() - locktime = self._read_le_uint32() - strDZeel = "" - if version >= 2: - strDZeel = self._read_varbytes() - txid = self.TX_HASH_FN(self.binary[start:self.cursor]) - return TxTime( - version=version, - time=time, - inputs=inputs, - outputs=outputs, - locktime=locktime, - txid_rev=txid, - wtxid_rev=txid, - ) - - def _read_tx_parts(self): - orig_start = self.cursor - start = self.cursor - marker = self.binary[self.cursor + 8] - if marker: # non-segwit - tx = self.read_tx_no_segwit() - return tx, self.cursor - orig_start - - version = self._read_le_int32() - time = self._read_le_uint32() - orig_ser = self.binary[start:self.cursor] - - marker = self._read_byte() - flag = self._read_byte() - - start = self.cursor - inputs = self._read_inputs() - outputs = self._read_outputs() - orig_ser += self.binary[start:self.cursor] - - witness_start = self.cursor - witness = self._read_witness(len(inputs)) - witness_size = self.cursor - witness_start + 2 # +2 due to marker and flag bytes - - start = self.cursor - locktime = self._read_le_uint32() - strDZeel = "" - - if version >= 2: - strDZeel = self._read_varbytes() - - base_size = self.cursor - orig_start - witness_size - weight = 4 * base_size + witness_size - vsize = weight // 4 + (weight % 4 > 0) - orig_ser += self.binary[start:self.cursor] - - txid = self.TX_HASH_FN(orig_ser) - wtxid = self.TX_HASH_FN(self.binary[orig_start:self.cursor]) - tx = TxTimeSegWit( - version=version, - time=time, - marker=marker, - flag=flag, - inputs=inputs, - outputs=outputs, - witness=witness, - locktime=locktime, - txid_rev=txid, - wtxid_rev=wtxid, - ) - return tx, vsize - - def read_tx(self): - return self._read_tx_parts()[0] - - -@dataclass(kw_only=True, slots=True) -class TxTrezarcoin(Tx): - '''Class representing transaction that has a time and txcomment field.''' - time: int - txcomment: bytes - - -class DeserializerTrezarcoin(Deserializer): - - def read_tx(self): - start = self.cursor - version = self._read_le_int32() - time = self._read_le_uint32() - inputs = self._read_inputs() - outputs = self._read_outputs() - locktime = self._read_le_uint32() - if version >= 2: - txcomment = self._read_varbytes() - else: - txcomment = b'' - txid = self.TX_HASH_FN(self.binary[start:self.cursor]) - return TxTrezarcoin( - version=version, - time=time, - inputs=inputs, - outputs=outputs, - locktime=locktime, - txcomment=txcomment, - txid_rev=txid, - wtxid_rev=txid, - ) - - @staticmethod - def blake2s_gen(data): - keyOne = data[36:46] - keyTwo = data[58:68] - ntime = data[68:72] - _nBits = data[72:76] - _nonce = data[76:80] - _full_merkle = data[36:68] - _input112 = data + _full_merkle - _key = keyTwo + ntime + _nBits + _nonce + keyOne - # Prepare 112Byte Header - blake2s_hash = blake2s(_input112, digest_size=32, key=_key) - # TrezarFlips - Only for Genesis - return ''.join(map(str.__add__, blake2s_hash.hexdigest()[-2::-2], - blake2s_hash.hexdigest()[-1::-2])) - - @staticmethod - def blake2s(data): - keyOne = data[36:46] - keyTwo = data[58:68] - ntime = data[68:72] - _nBits = data[72:76] - _nonce = data[76:80] - _full_merkle = data[36:68] - _input112 = data + _full_merkle - _key = keyTwo + ntime + _nBits + _nonce + keyOne - # Prepare 112Byte Header - blake2s_hash = blake2s(_input112, digest_size=32, key=_key) - # TrezarFlips - return blake2s_hash.digest() - - -class DeserializerBlackcoin(Deserializer): - BLACKCOIN_TX_VERSION = 2 - - def _get_version(self): - result, = unpack_le_int32_from(self.binary, self.cursor) - return result - - def read_tx(self): - orig_start = self.cursor - start = self.cursor - version = self._get_version() - if version < self.BLACKCOIN_TX_VERSION: - tx = TxTime( - version=self._read_le_int32(), - time=self._read_le_uint32(), - inputs=self._read_inputs(), - outputs=self._read_outputs(), - locktime=self._read_le_uint32(), - txid_rev=None, - wtxid_rev=None, - ) - else: - tx = Tx( - version=self._read_le_int32(), - inputs=self._read_inputs(), - outputs=self._read_outputs(), - locktime=self._read_le_uint32(), - txid_rev=None, - wtxid_rev=None, - ) - tx.txid_rev = tx.wtxid_rev = self.TX_HASH_FN(self.binary[orig_start:self.cursor]) - return tx - - -class DeserializerReddcoin(Deserializer): - def read_tx(self): - start = self.cursor - version = self._read_le_int32() - inputs = self._read_inputs() - outputs = self._read_outputs() - locktime = self._read_le_uint32() - if version > 1: - time = self._read_le_uint32() - else: - time = 0 - - txid = self.TX_HASH_FN(self.binary[start:self.cursor]) - return TxTime( - version=version, - time=time, - inputs=inputs, - outputs=outputs, - locktime=locktime, - txid_rev=txid, - wtxid_rev=txid, - ) - - -class DeserializerVerge(Deserializer): - def read_tx(self): - start = self.cursor - version = self._read_le_int32() - time = self._read_le_uint32() - inputs = self._read_inputs() - outputs = self._read_outputs() - locktime = self._read_le_uint32() - - txid = self.TX_HASH_FN(self.binary[start:self.cursor]) - return TxTime( - version=version, - time=time, - inputs=inputs, - outputs=outputs, - locktime=locktime, - txid_rev=txid, - wtxid_rev=txid, - ) - - -class DeserializerEmercoin(DeserializerTxTimeSegWit): - VERSION_AUXPOW = (1 << 8) - - def is_merged_block(self): - start = self.cursor - self.cursor = 0 - version = self._read_le_uint32() - self.cursor = start - if version & self.VERSION_AUXPOW: - return True - return False - - def read_header(self, static_header_size): - '''Return the AuxPow block header bytes''' - start = self.cursor - version = self._read_le_uint32() - if version & self.VERSION_AUXPOW: - # We are going to calculate the block size then read it as bytes - self.cursor = start - self.cursor += static_header_size # Block normal header - self.read_tx() # AuxPow transaction - self.cursor += 32 # Parent block hash - merkle_size = self._read_varint() - self.cursor += 32 * merkle_size # Merkle branch - self.cursor += 4 # Index - merkle_size = self._read_varint() - self.cursor += 32 * merkle_size # Chain merkle branch - self.cursor += 4 # Chain index - self.cursor += 80 # Parent block header - header_end = self.cursor - else: - header_end = static_header_size - self.cursor = start - return self._read_nbytes(header_end) - - -class DeserializerBitcoinAtom(DeserializerSegWit): - FORK_BLOCK_HEIGHT = 505888 - - def read_header(self, height, static_header_size): - '''Return the block header bytes''' - header_len = static_header_size - if height >= self.FORK_BLOCK_HEIGHT: - header_len += 4 # flags - return self._read_nbytes(header_len) - - -class DeserializerGroestlcoin(DeserializerSegWit): - TX_HASH_FN = staticmethod(sha256) - - -class TxInputTokenPay(TxInput): - '''Class representing a TokenPay transaction input.''' - - OP_ANON_MARKER = 0xb9 - # 2byte marker (cpubkey + sigc + sigr) - MIN_ANON_IN_SIZE = 2 + (33 + 32 + 32) - - def _is_anon_input(self): - return (len(self.script) >= self.MIN_ANON_IN_SIZE and - self.script[0] == OpCodes.OP_RETURN and - self.script[1] == self.OP_ANON_MARKER) - - def is_generation(self): - # Transactions coming in from stealth addresses are seen by - # the blockchain as newly minted coins. The reverse, where coins - # are sent TO a stealth address, are seen by the blockchain as - # a coin burn. - if self._is_anon_input(): - return True - return super(TxInputTokenPay, self).is_generation() - - -@dataclass(kw_only=True, slots=True) -class TxInputTokenPayStealth: - '''Class representing a TokenPay stealth transaction input.''' - keyimage: bytes - ringsize: bytes - script: bytes - sequence: int - - def __str__(self): - script = self.script.hex() - keyimage = bytes(self.keyimage).hex() - return (f"Input({keyimage}, {self.ringsize[1]:d}, script={script}, " - f"sequence={self.sequence:d})") - - def is_generation(self): - return True - - def serialize(self): - return b''.join(( - self.keyimage, - self.ringsize, - pack_varbytes(self.script), - pack_le_uint32(self.sequence), - )) - - -class DeserializerTokenPay(DeserializerTxTime): - - def _read_input(self): - txin = TxInputTokenPay( - prev_txid_rev=self._read_nbytes(32), - prev_idx=self._read_le_uint32(), - script=self._read_varbytes(), - sequence=self._read_le_uint32(), - ) - if txin._is_anon_input(): - # Not sure if this is actually needed, and seems - # extra work for no immediate benefit, but it at - # least correctly represents a stealth input - raw = txin.serialize() - deserializer = Deserializer(raw) - txin = TxInputTokenPayStealth( - keyimage=deserializer._read_nbytes(33), - ringsize=deserializer._read_nbytes(3), - script=deserializer._read_varbytes(), - sequence=deserializer._read_le_uint32(), - ) - return txin - - -# Decred -@dataclass(kw_only=True, slots=True) -class TxInputDcr: - '''Class representing a Decred transaction input.''' - prev_txid_rev: bytes - prev_idx: int - tree: int - sequence: int - - def __str__(self): - prev_hash = hash_to_hex_str(self.prev_txid_rev) - return (f"Input({prev_hash}, {self.prev_idx:d}, tree={self.tree}, " - f"sequence={self.sequence:d})") - - def is_generation(self): - '''Test if an input is generation/coinbase like''' - return self.prev_idx == MINUS_1 and self.prev_txid_rev == ZERO - - -@dataclass(kw_only=True, slots=True) -class TxOutputDcr: - '''Class representing a Decred transaction output.''' - value: int - version: int - pk_script: bytes - - -@dataclass(kw_only=True, slots=True) -class TxDcr(Tx): - '''Class representing a Decred transaction.''' - expiry: int - witness: Sequence - - -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()[0] - - def read_tx_block(self): - read = self.read_tx - txs = [read() for _ in range(self._read_varint())] - stxs = [read() for _ in range(self._read_varint())] - return txs + stxs - - 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( - prev_txid_rev=self._read_nbytes(32), - prev_idx=self._read_le_uint32(), - tree=self._read_byte(), - sequence=self._read_le_uint32(), - ) - - def _read_output(self): - return TxOutputDcr( - value=self._read_le_int64(), - version=self._read_le_uint16(), - pk_script=self._read_varbytes(), - ) - - def _read_witness(self, fields): - read_witness_field = self._read_witness_field - assert fields == self._read_varint() - return [read_witness_field() for _ in range(fields)] - - def _read_witness_field(self): - value_in = self._read_le_int64() - block_height = self._read_le_uint32() - block_index = self._read_le_uint32() - script = self._read_varbytes() - return value_in, block_height, block_index, script - - def _read_tx_parts(self): - 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() - end_prefix = self.cursor - witness = self._read_witness(len(inputs)) - - # TxSerializeNoWitness << 16 == 0x10000 - no_witness_header = pack_le_uint32(0x10000 | (version & 0xffff)) - prefix_tx = no_witness_header + self.binary[start+4:end_prefix] - txid_rev = self.blake256(prefix_tx) - - return TxDcr( - version=version, - inputs=inputs, - outputs=outputs, - locktime=locktime, - expiry=expiry, - witness=witness, - txid_rev=txid_rev, - wtxid_rev=txid_rev, - ), self.cursor - start - - -class DeserializerSmartCash(Deserializer): - TX_HASH_FN = staticmethod(sha256) - - @staticmethod - def keccak(data): - from Cryptodome.Hash import keccak - keccak_hash = keccak.new(data=data, digest_bits=256) - return keccak_hash.digest() - - -@dataclass(kw_only=True, slots=True) -class TxBitcoinDiamond(Tx): - '''Class representing a transaction.''' - preblockhash: str - - -class DeserializerBitcoinDiamond(Deserializer): - bitcoin_diamond_tx_version = 12 - - def read_tx(self): - # Return a Deserialized TX. - start = self.cursor - version = self._get_version() - if version != self.bitcoin_diamond_tx_version: - tx = Tx( - version=self._read_le_int32(), - inputs=self._read_inputs(), - outputs=self._read_outputs(), - locktime=self._read_le_uint32(), - txid_rev=None, - wtxid_rev=None, - ) - else: - tx = TxBitcoinDiamond( - version=self._read_le_int32(), - preblockhash=hash_to_hex_str(self._read_nbytes(32)), - inputs=self._read_inputs(), - outputs=self._read_outputs(), - locktime=self._read_le_uint32(), - txid_rev=None, - wtxid_rev=None, - ) - txid = self.TX_HASH_FN(self.binary[start:self.cursor]) - tx.txid_rev = tx.wtxid_rev = txid - return tx - - def _get_version(self): - result, = unpack_le_int32_from(self.binary, self.cursor) - return result - - -@dataclass(kw_only=True, slots=True) -class TxBitcoinDiamondSegWit(TxSegWit): - '''Class representing a SegWit transaction.''' - preblockhash: str - - -class DeserializerBitcoinDiamondSegWit(DeserializerBitcoinDiamond, - DeserializerSegWit): - def _read_tx_parts(self): - orig_start = self.cursor - start = self.cursor - tx_version = self._get_version() - if tx_version == self.bitcoin_diamond_tx_version: - marker = self.binary[self.cursor + 4 + 32] - else: - marker = self.binary[self.cursor + 4] - - if marker: # non-segwit - tx = DeserializerBitcoinDiamond.read_tx(self) - return tx, self.cursor - orig_start - - # Ugh, this is nasty. - version = self._read_le_int32() - present_block_hash = None - if version == self.bitcoin_diamond_tx_version: - present_block_hash = hash_to_hex_str(self._read_nbytes(32)) - orig_ser = self.binary[start:self.cursor] - - marker = self._read_byte() - flag = self._read_byte() - - start = self.cursor - inputs = self._read_inputs() - outputs = self._read_outputs() - orig_ser += self.binary[start:self.cursor] - - witness_start = self.cursor - witness = self._read_witness(len(inputs)) - witness_size = self.cursor - witness_start + 2 # +2 due to marker and flag bytes - - start = self.cursor - locktime = self._read_le_uint32() - orig_ser += self.binary[start:self.cursor] - base_size = self.cursor - orig_start - witness_size - weight = 4 * base_size + witness_size - vsize = weight // 4 + (weight % 4 > 0) - - txid = self.TX_HASH_FN(orig_ser) - wtxid = self.TX_HASH_FN(self.binary[orig_start:self.cursor]) - - if present_block_hash is not None: - return TxBitcoinDiamondSegWit( - version=version, - preblockhash=present_block_hash, - marker=marker, - flag=flag, - inputs=inputs, - outputs=outputs, - witness=witness, - locktime=locktime, - txid_rev=txid, - wtxid_rev=wtxid), vsize - else: - return TxSegWit( - version=version, - marker=marker, - flag=flag, - inputs=inputs, - outputs=outputs, - witness=witness, - locktime=locktime, - txid_rev=txid, - wtxid_rev=wtxid), vsize - - def read_tx(self): - '''Return a (Deserialized TX, TX_HASH) pair. - - The hash needs to be reversed for human display; for efficiency - we process it in the natural serialized order. - ''' - return self._read_tx_parts()[0] - - -class DeserializerElectra(Deserializer): - ELECTRA_TX_VERSION = 7 - - def _get_version(self): - result, = unpack_le_int32_from(self.binary, self.cursor) - return result - - def read_tx(self): - orig_start = self.cursor - start = self.cursor - version = self._get_version() - if version != self.ELECTRA_TX_VERSION: - tx = TxTime( - version=self._read_le_int32(), - time=self._read_le_uint32(), - inputs=self._read_inputs(), - outputs=self._read_outputs(), - locktime=self._read_le_uint32(), - txid_rev=None, - wtxid_rev=None, - ) - else: - tx = Tx( - version=self._read_le_int32(), - inputs=self._read_inputs(), - outputs=self._read_outputs(), - locktime=self._read_le_uint32(), - txid_rev=None, - wtxid_rev=None, - ) - tx.txid_rev = tx.wtxid_rev = self.TX_HASH_FN(self.binary[orig_start:self.cursor]) - return tx - - -class DeserializerECCoin(Deserializer): - def read_tx(self): - orig_start = self.cursor - start = self.cursor - tx_version = self._read_le_int32() - tx = TxTime( - version=tx_version, - time=self._read_le_uint32(), - inputs=self._read_inputs(), - outputs=self._read_outputs(), - locktime=self._read_le_uint32(), - txid_rev=None, - wtxid_rev=None, - ) - - if tx_version > 1: - self.cursor += 32 - - tx.txid_rev = tx.wtxid_rev = self.TX_HASH_FN(self.binary[orig_start:self.cursor]) - return tx - - -class DeserializerZcoin(Deserializer): - def _read_input(self): - tx_input = TxInput( - prev_txid_rev=self._read_nbytes(32), - prev_idx=self._read_le_uint32(), - script=self._read_varbytes(), - sequence=self._read_le_uint32(), - ) - - if tx_input.prev_idx == MINUS_1 and tx_input.prev_txid_rev == ZERO: - return tx_input - - if tx_input.script[0] == 0xc4: # This is a Sigma spend - mimic a generation tx - return TxInput( - prev_txid_rev=ZERO, - prev_idx=MINUS_1, - script=tx_input.script, - sequence=tx_input.sequence - ) - - return tx_input - - -class DeserializerXaya(DeserializerSegWit, DeserializerAuxPow): - """Deserializer class for the Xaya network - - The main difference to other networks is the changed format of the - block header with "triple purpose mining", see - https://github.com/xaya/xaya/blob/master/doc/xaya/mining.md. - - This builds upon classic auxpow, but has a modified serialisation format - that we have to implement here.""" - - MM_FLAG = 0x80 - - def read_header(self, static_header_size): - """Reads in the full block header (including PoW data)""" - - # We first calculate the dynamic size of the block header, and then - # read in all the data in the final step. - start = self.cursor - - self.cursor += static_header_size # Normal block header - - algo = self._read_byte() - self._read_le_uint32() # nBits - - if algo & self.MM_FLAG: - self.read_auxpow() - else: - self.cursor += static_header_size # Fake header - - end = self.cursor - self.cursor = start - return self._read_nbytes(end - start) - - -class DeserializerSimplicity(Deserializer): - SIMPLICITY_TX_VERSION = 3 - - def _get_version(self): - result, = unpack_le_int32_from(self.binary, self.cursor) - return result - - def read_tx(self): - orig_start = self.cursor - start = self.cursor - version = self._get_version() - if version < self.SIMPLICITY_TX_VERSION: - tx = TxTime( - version=self._read_le_int32(), - time=self._read_le_uint32(), - inputs=self._read_inputs(), - outputs=self._read_outputs(), - locktime=self._read_le_uint32(), - txid_rev=None, - wtxid_rev=None, - ) - else: - tx = Tx( - version=self._read_le_int32(), - inputs=self._read_inputs(), - outputs=self._read_outputs(), - locktime=self._read_le_uint32(), - txid_rev=None, - wtxid_rev=None, - ) - tx.txid_rev = tx.wtxid_rev = self.TX_HASH_FN(self.binary[orig_start:self.cursor]) - return tx - - -class DeserializerPrimecoin(Deserializer): - def read_header(self, static_header_size): - '''Return the block header bytes''' - start = self.cursor - # Decode the block header size including multiplier then read it as bytes - self.cursor += static_header_size - multiplier_size = self._read_varint() - self.cursor += multiplier_size - header_end = self.cursor - self.cursor = start - return self._read_nbytes(header_end - start) diff --git a/src/electrumx/lib/tx_axe.py b/src/electrumx/lib/tx_axe.py deleted file mode 100644 index 180172c..0000000 --- a/src/electrumx/lib/tx_axe.py +++ /dev/null @@ -1,489 +0,0 @@ -# Copyright (c) 2016-2018, Neil Booth -# Copyright (c) 2018, the ElectrumX authors -# -# All rights reserved. -# -# The MIT License (MIT) -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -'''Deserializer for AXE DIP2 special transaction types''' - -from dataclasses import dataclass -from typing import Any - -from electrumx.lib.tx import Deserializer, Tx -from electrumx.lib.util import (pack_le_uint16, pack_le_int32, pack_le_uint32, - pack_le_int64, pack_varint, pack_varbytes, - pack_be_uint16) - - -# https://github.com/dashpay/dips/blob/master/dip-0002.md -@dataclass(kw_only=True, slots=True) -class AxeTx(Tx): - '''Class representing a Axe transaction''' - tx_type: int - extra_payload: Any - - def serialize(self): - nLocktime = pack_le_uint32(self.locktime) - txins = (pack_varint(len(self.inputs)) + - b''.join(tx_in.serialize() for tx_in in self.inputs)) - txouts = (pack_varint(len(self.outputs)) + - b''.join(tx_out.serialize() for tx_out in self.outputs)) - - if self.tx_type: - uVersion = pack_le_uint16(self.version) - uTxType = pack_le_uint16(self.tx_type) - vExtra = self._serialize_extra_payload() - return uVersion + uTxType + txins + txouts + nLocktime + vExtra - else: - nVersion = pack_le_int32(self.version) - return nVersion + txins + txouts + nLocktime - - def _serialize_extra_payload(self): - extra = self.extra_payload - spec_tx_class = DeserializerAxe.SPEC_TX_HANDLERS.get(self.tx_type) - if not spec_tx_class: - assert isinstance(extra, (bytes, bytearray)) - return pack_varbytes(extra) - - if not isinstance(extra, spec_tx_class): - raise ValueError('Axe tx_type does not conform with extra' - ' payload class: %s, %s' % (self.tx_type, extra)) - return pack_varbytes(extra.serialize()) - - -# https://github.com/dashpay/dips/blob/master/dip-0002-special-transactions.md -@dataclass(kw_only=True, slots=True) -class AxeProRegTx: - '''Class representing DIP3 ProRegTx''' - version: int - type: int - mode: int - collateralOutpoint: 'TxOutPoint' - ipAddress: bytes - port: int - KeyIdOwner: bytes - PubKeyOperator: bytes - KeyIdVoting: bytes - operatorReward: int - scriptPayout: bytes - inputsHash: bytes - payloadSig: bytes - - def serialize(self): - assert (len(self.ipAddress) == 16 - and len(self.KeyIdOwner) == 20 - and len(self.PubKeyOperator) == 48 - and len(self.KeyIdVoting) == 20 - and len(self.inputsHash) == 32) - return ( - pack_le_uint16(self.version) + # version - pack_le_uint16(self.type) + # type - pack_le_uint16(self.mode) + # mode - self.collateralOutpoint.serialize() + # collateralOutpoint - self.ipAddress + # ipAddress - pack_be_uint16(self.port) + # port - self.KeyIdOwner + # KeyIdOwner - self.PubKeyOperator + # PubKeyOperator - self.KeyIdVoting + # KeyIdVoting - pack_le_uint16(self.operatorReward) + # operatorReward - pack_varbytes(self.scriptPayout) + # scriptPayout - self.inputsHash + # inputsHash - pack_varbytes(self.payloadSig) # payloadSig - ) - - @classmethod - def read_tx_extra(cls, deser): - return AxeProRegTx( - version=deser._read_le_uint16(), - type=deser._read_le_uint16(), - mode=deser._read_le_uint16(), - collateralOutpoint=deser._read_outpoint(), - ipAddress=deser._read_nbytes(16), - port=deser._read_be_uint16(), - KeyIdOwner=deser._read_nbytes(20), - PubKeyOperator=deser._read_nbytes(48), - KeyIdVoting=deser._read_nbytes(20), - operatorReward=deser._read_le_uint16(), - scriptPayout=deser._read_varbytes(), - inputsHash=deser._read_nbytes(32), - payloadSig=deser._read_varbytes(), - ) - - -@dataclass(kw_only=True, slots=True) -class AxeProUpServTx: - '''Class representing DIP3 ProUpServTx''' - version: int - proTxHash: bytes - ipAddress: bytes - port: int - scriptOperatorPayout: bytes - inputsHash: bytes - payloadSig: bytes - - def serialize(self): - assert (len(self.proTxHash) == 32 - and len(self.ipAddress) == 16 - and len(self.inputsHash) == 32 - and len(self.payloadSig) == 96) - return ( - pack_le_uint16(self.version) + # version - self.proTxHash + # proTxHash - self.ipAddress + # ipAddress - pack_be_uint16(self.port) + # port - pack_varbytes(self.scriptOperatorPayout) + # scriptOperatorPayout - self.inputsHash + # inputsHash - self.payloadSig # payloadSig - ) - - @classmethod - def read_tx_extra(cls, deser): - return AxeProUpServTx( - version=deser._read_le_uint16(), - proTxHash=deser._read_nbytes(32), - ipAddress=deser._read_nbytes(16), - port=deser._read_be_uint16(), - scriptOperatorPayout=deser._read_varbytes(), - inputsHash=deser._read_nbytes(32), - payloadSig=deser._read_nbytes(96), - ) - - -@dataclass(kw_only=True, slots=True) -class AxeProUpRegTx: - '''Class representing DIP3 ProUpRegTx''' - version: int - proTxHash: bytes - mode: int - PubKeyOperator: bytes - KeyIdVoting: bytes - scriptPayout: bytes - inputsHash: bytes - payloadSig: bytes - - def serialize(self): - assert (len(self.proTxHash) == 32 - and len(self.PubKeyOperator) == 48 - and len(self.KeyIdVoting) == 20 - and len(self.inputsHash) == 32) - return ( - pack_le_uint16(self.version) + # version - self.proTxHash + # proTxHash - pack_le_uint16(self.mode) + # mode - self.PubKeyOperator + # PubKeyOperator - self.KeyIdVoting + # KeyIdVoting - pack_varbytes(self.scriptPayout) + # scriptPayout - self.inputsHash + # inputsHash - pack_varbytes(self.payloadSig) # payloadSig - ) - - @classmethod - def read_tx_extra(cls, deser): - return AxeProUpRegTx( - version=deser._read_le_uint16(), - proTxHash=deser._read_nbytes(32), - mode=deser._read_le_uint16(), - PubKeyOperator=deser._read_nbytes(48), - KeyIdVoting=deser._read_nbytes(20), - scriptPayout=deser._read_varbytes(), - inputsHash=deser._read_nbytes(32), - payloadSig=deser._read_varbytes(), - ) - - -@dataclass(kw_only=True, slots=True) -class AxeProUpRevTx: - '''Class representing DIP3 ProUpRevTx''' - version: int - proTxHash: bytes - reason: int - inputsHash: bytes - payloadSig: bytes - - def serialize(self): - assert (len(self.proTxHash) == 32 - and len(self.inputsHash) == 32 - and len(self.payloadSig) == 96) - return ( - pack_le_uint16(self.version) + # version - self.proTxHash + # proTxHash - pack_le_uint16(self.reason) + # reason - self.inputsHash + # inputsHash - self.payloadSig # payloadSig - ) - - @classmethod - def read_tx_extra(cls, deser): - return AxeProUpRevTx( - version=deser._read_le_uint16(), - proTxHash=deser._read_nbytes(32), - reason=deser._read_le_uint16(), - inputsHash=deser._read_nbytes(32), - payloadSig=deser._read_nbytes(96), - ) - - -@dataclass(kw_only=True, slots=True) -class AxeCbTx: - '''Class representing DIP4 coinbase special tx''' - version: int - height: int - merkleRootMNList: bytes - merkleRootQuorums: bytes - - def serialize(self): - assert len(self.merkleRootMNList) == 32 - res = ( - pack_le_uint16(self.version) + # version - pack_le_uint32(self.height) + # height - self.merkleRootMNList # merkleRootMNList - ) - if self.version > 1: - assert len(self.merkleRootQuorums) == 32 - res += self.merkleRootQuorums # merkleRootQuorums - return res - - @classmethod - def read_tx_extra(cls, deser): - version = deser._read_le_uint16() - height = deser._read_le_uint32() - merkleRootMNList = deser._read_nbytes(32) - merkleRootQuorums = b'' - if version > 1: - merkleRootQuorums = deser._read_nbytes(32) - return AxeCbTx( - version=version, - height=height, - merkleRootMNList=merkleRootMNList, - merkleRootQuorums=merkleRootQuorums, - ) - - -@dataclass(kw_only=True, slots=True) -class AxeSubTxRegister: - '''Class representing DIP5 SubTxRegister''' - version: int - userName: bytes - pubKey: bytes - payloadSig: bytes - - def serialize(self): - assert (len(self.pubKey) == 48 - and len(self.payloadSig) == 96) - return ( - pack_le_uint16(self.version) + # version - pack_varbytes(self.userName) + # userName - self.pubKey + # pubKey - self.payloadSig # payloadSig - ) - - @classmethod - def read_tx_extra(cls, deser): - return AxeSubTxRegister( - version=deser._read_le_uint16(), - userName=deser._read_varbytes(), - pubKey=deser._read_nbytes(48), - payloadSig=deser._read_nbytes(96), - ) - - -@dataclass(kw_only=True, slots=True) -class AxeSubTxTopup: - '''Class representing DIP5 SubTxTopup''' - version: int - regTxHash: bytes - - def serialize(self): - assert len(self.regTxHash) == 32 - return ( - pack_le_uint16(self.version) + # version - self.regTxHash # regTxHash - ) - - @classmethod - def read_tx_extra(cls, deser): - return AxeSubTxTopup( - version=deser._read_le_uint16(), - regTxHash=deser._read_nbytes(32), - ) - - -@dataclass(kw_only=True, slots=True) -class AxeSubTxResetKey: - '''Class representing DIP5 SubTxResetKey''' - version: int - regTxHash: bytes - hashPrevSubTx: bytes - creditFee: int - newPubKey: bytes - payloadSig: bytes - - def serialize(self): - assert (len(self.regTxHash) == 32 - and len(self.hashPrevSubTx) == 32 - and len(self.newPubKey) == 48 - and len(self.payloadSig) == 96) - return ( - pack_le_uint16(self.version) + # version - self.regTxHash + # regTxHash - self.hashPrevSubTx + # hashPrevSubTx - pack_le_int64(self.creditFee) + # creditFee - self.newPubKey + # newPubKey - self.payloadSig # payloadSig - ) - - @classmethod - def read_tx_extra(cls, deser): - return AxeSubTxResetKey( - version=deser._read_le_uint16(), - regTxHash=deser._read_nbytes(32), - hashPrevSubTx=deser._read_nbytes(32), - creditFee=deser._read_le_int64(), - newPubKey=deser._read_nbytes(48), - payloadSig=deser._read_nbytes(96), - ) - - -@dataclass(kw_only=True, slots=True) -class AxeSubTxCloseAccount: - '''Class representing DIP5 SubTxCloseAccount''' - version: int - regTxHash: bytes - hashPrevSubTx: bytes - creditFee: int - payloadSig: bytes - - def serialize(self): - assert (len(self.regTxHash) == 32 - and len(self.hashPrevSubTx) == 32 - and len(self.payloadSig) == 96) - return ( - pack_le_uint16(self.version) + # version - self.regTxHash + # regTxHash - self.hashPrevSubTx + # hashPrevSubTx - pack_le_int64(self.creditFee) + # creditFee - self.payloadSig # payloadSig - ) - - @classmethod - def read_tx_extra(cls, deser): - return AxeSubTxCloseAccount( - version=deser._read_le_uint16(), - regTxHash=deser._read_nbytes(32), - hashPrevSubTx=deser._read_nbytes(32), - creditFee=deser._read_le_int64(), - payloadSig=deser._read_nbytes(96), - ) - - -# https://dash-docs.github.io/en/developer-reference#outpoint -@dataclass(kw_only=True, slots=True) -class TxOutPoint: - '''Class representing tx output outpoint''' - hash: bytes - index: int - - def serialize(self): - assert len(self.hash) == 32 - return ( - self.hash + # hash - pack_le_uint32(self.index) # index - ) - - @classmethod - def read_outpoint(cls, deser): - return TxOutPoint( - hash=deser._read_nbytes(32), - index=deser._read_le_uint32(), - ) - - -class DeserializerAxe(Deserializer): - '''Deserializer for AXE DIP2 special tx types''' - # Supported Spec Tx types and corresponding classes mapping - PRO_REG_TX = 1 - PRO_UP_SERV_TX = 2 - PRO_UP_REG_TX = 3 - PRO_UP_REV_TX = 4 - CB_TX = 5 - SUB_TX_REGISTER = 8 - SUB_TX_TOPUP = 9 - SUB_TX_RESET_KEY = 10 - SUB_TX_CLOSE_ACCOUNT = 11 - - SPEC_TX_HANDLERS = { - PRO_REG_TX: AxeProRegTx, - PRO_UP_SERV_TX: AxeProUpServTx, - PRO_UP_REG_TX: AxeProUpRegTx, - PRO_UP_REV_TX: AxeProUpRevTx, - CB_TX: AxeCbTx, - SUB_TX_REGISTER: AxeSubTxRegister, - SUB_TX_TOPUP: AxeSubTxTopup, - SUB_TX_RESET_KEY: AxeSubTxResetKey, - SUB_TX_CLOSE_ACCOUNT: AxeSubTxCloseAccount, - } - - def _read_outpoint(self): - return TxOutPoint.read_outpoint(self) - - def read_tx(self): - start = self.cursor - header = self._read_le_uint32() - tx_type = header >> 16 # DIP2 tx type - if tx_type: - version = header & 0x0000ffff - else: - version = header - - if tx_type and version < 3: - version = header - tx_type = 0 - - inputs = self._read_inputs() - outputs = self._read_outputs() - locktime = self._read_le_uint32() - if tx_type: - extra_payload_size = self._read_varint() - end = self.cursor + extra_payload_size - spec_tx_class = DeserializerAxe.SPEC_TX_HANDLERS.get(tx_type) - if spec_tx_class: - read_method = getattr(spec_tx_class, 'read_tx_extra', None) - extra_payload = read_method(self) - assert isinstance(extra_payload, spec_tx_class) - else: - extra_payload = self._read_nbytes(extra_payload_size) - assert self.cursor == end - else: - extra_payload = b'' - txid = self.TX_HASH_FN(self.binary[start:self.cursor]) - tx = AxeTx( - version=version, - inputs=inputs, - outputs=outputs, - locktime=locktime, - tx_type=tx_type, - extra_payload=extra_payload, - txid_rev=txid, - wtxid_rev=txid, - ) - return tx diff --git a/src/electrumx/lib/tx_dash.py b/src/electrumx/lib/tx_dash.py deleted file mode 100644 index b2b6829..0000000 --- a/src/electrumx/lib/tx_dash.py +++ /dev/null @@ -1,442 +0,0 @@ -# Copyright (c) 2016-2018, Neil Booth -# Copyright (c) 2018, the ElectrumX authors -# -# All rights reserved. -# -# The MIT License (MIT) -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -'''Deserializer for Dash DIP2 special transaction types''' - -from dataclasses import dataclass -from typing import Any - -from electrumx.lib.tx import Deserializer, Tx -from electrumx.lib.util import (pack_le_uint16, pack_le_int32, pack_le_uint32, - pack_le_int64, pack_varint, pack_varbytes, - pack_be_uint16) - - -# https://github.com/dashpay/dips/blob/master/dip-0002.md -@dataclass(kw_only=True, slots=True) -class DashTx(Tx): - '''Class representing a Dash transaction''' - tx_type: int - extra_payload: Any - - def serialize(self): - nLocktime = pack_le_uint32(self.locktime) - txins = (pack_varint(len(self.inputs)) + - b''.join(tx_in.serialize() for tx_in in self.inputs)) - txouts = (pack_varint(len(self.outputs)) + - b''.join(tx_out.serialize() for tx_out in self.outputs)) - - if self.tx_type: - uVersion = pack_le_uint16(self.version) - uTxType = pack_le_uint16(self.tx_type) - vExtra = self._serialize_extra_payload() - return uVersion + uTxType + txins + txouts + nLocktime + vExtra - else: - nVersion = pack_le_int32(self.version) - return nVersion + txins + txouts + nLocktime - - def _serialize_extra_payload(self): - extra = self.extra_payload - spec_tx_class = DeserializerDash.SPEC_TX_HANDLERS.get(self.tx_type) - if not spec_tx_class: - assert isinstance(extra, (bytes, bytearray)) - return pack_varbytes(extra) - - if not isinstance(extra, spec_tx_class): - raise ValueError('Dash tx_type does not conform with extra' - ' payload class: %s, %s' % (self.tx_type, extra)) - return pack_varbytes(extra.serialize()) - - -# https://github.com/dashpay/dips/blob/master/dip-0002-special-transactions.md -@dataclass(kw_only=True, slots=True) -class DashProRegTx: - '''Class representing DIP3 ProRegTx''' - version: int - type: int - mode: int - collateralOutpoint: 'TxOutPoint' - ipAddress: bytes - port: int - KeyIdOwner: bytes - PubKeyOperator: bytes - KeyIdVoting: bytes - operatorReward: int - scriptPayout: bytes - inputsHash: bytes - platformNodeID: bytes - platformP2PPort: int - platformHTTPPort: int - payloadSig: bytes - - def serialize(self): - assert (len(self.ipAddress) == 16 - and len(self.KeyIdOwner) == 20 - and len(self.PubKeyOperator) == 48 - and len(self.KeyIdVoting) == 20 - and len(self.inputsHash) == 32) - res = ( - pack_le_uint16(self.version) + # version - pack_le_uint16(self.type) + # type - pack_le_uint16(self.mode) + # mode - self.collateralOutpoint.serialize() + # collateralOutpoint - self.ipAddress + # ipAddress - pack_be_uint16(self.port) + # port - self.KeyIdOwner + # KeyIdOwner - self.PubKeyOperator + # PubKeyOperator - self.KeyIdVoting + # KeyIdVoting - pack_le_uint16(self.operatorReward) + # operatorReward - pack_varbytes(self.scriptPayout) + # scriptPayout - self.inputsHash) # inputsHash - if self.version >= 2: - res += (self.platformNodeID + # platformNodeID - pack_le_uint16(self.platformP2PPort) + # platformP2PPort - pack_le_uint16(self.platformHTTPPort)) # platformHTTPPort - res += pack_varbytes(self.payloadSig) # payloadSig - return res - - @classmethod - def read_tx_extra(cls, deser): - version = deser._read_le_uint16() # version - ntype = deser._read_le_uint16() # type - mode = deser._read_le_uint16() # mode - collateralOutpoint = deser._read_outpoint() # collateralOutpoint - ipAddress = deser._read_nbytes(16) # ipAddress - port = deser._read_be_uint16() # port - KeyIdOwner = deser._read_nbytes(20) # KeyIdOwner - PubKeyOperator = deser._read_nbytes(48) # PubKeyOperator - KeyIdVoting = deser._read_nbytes(20) # KeyIdVoting - operatorReward = deser._read_le_uint16() # operatorReward - scriptPayout = deser._read_varbytes() # scriptPayout - inputsHash = deser._read_nbytes(32) # inputsHash - platformNodeID = b'' - platformP2PPort = 0 - platformHTTPPort = 0 - if version >= 2 and ntype == 1: - platformNodeID = deser._read_nbytes(20) # platformNodeID - platformP2PPort = deser._read_le_uint16() # platformP2PPort - platformHTTPPort = deser._read_le_uint16() # platformHTTPPort - payloadSig = deser._read_varbytes() # payloadSig - return DashProRegTx( - version=version, - type=ntype, - mode=mode, - collateralOutpoint=collateralOutpoint, - ipAddress=ipAddress, - port=port, - KeyIdOwner=KeyIdOwner, - PubKeyOperator=PubKeyOperator, - KeyIdVoting=KeyIdVoting, - operatorReward=operatorReward, - scriptPayout=scriptPayout, - inputsHash=inputsHash, - platformNodeID=platformNodeID, - platformP2PPort=platformP2PPort, - platformHTTPPort=platformHTTPPort, - payloadSig=payloadSig, - ) - - -@dataclass(kw_only=True, slots=True) -class DashProUpServTx: - '''Class representing DIP3 ProUpServTx''' - version: int - type: int - proTxHash: bytes - ipAddress: bytes - port: int - scriptOperatorPayout: bytes - inputsHash: bytes - platformNodeID: bytes - platformP2PPort: int - platformHTTPPort: int - payloadSig: bytes - - def serialize(self): - assert (len(self.proTxHash) == 32 - and len(self.ipAddress) == 16 - and len(self.inputsHash) == 32 - and len(self.payloadSig) == 96) - res = pack_le_uint16(self.version) # version - if self.version >= 2: - res += pack_le_uint16(self.type) # type - res += ( - self.proTxHash + # proTxHash - self.ipAddress + # ipAddress - pack_be_uint16(self.port) + # port - pack_varbytes(self.scriptOperatorPayout) + # scriptOperatorPayout - self.inputsHash) # inputsHash - if self.version >= 2: - res += (self.platformNodeID + # platformNodeID - pack_le_uint16(self.platformP2PPort) + # platformP2PPort - pack_le_uint16(self.platformHTTPPort)) # platformHTTPPort - res += self.payloadSig # payloadSig - return res - - @classmethod - def read_tx_extra(cls, deser): - version = deser._read_le_uint16() # version - ntype = 0 - if version >= 2: - ntype = deser._read_le_uint16() # type - proTxHash = deser._read_nbytes(32) # proTxHash - ipAddress = deser._read_nbytes(16) # ipAddress - port = deser._read_be_uint16() # port - scriptOperatorPayout = deser._read_varbytes() # scriptOperatorPayout - inputsHash = deser._read_nbytes(32) # inputsHash - platformNodeID = b'' - platformP2PPort = 0 - platformHTTPPort = 0 - if version >= 2 and ntype == 1: - platformNodeID = deser._read_nbytes(20) # platformNodeID - platformP2PPort = deser._read_le_uint16() # platformP2PPort - platformHTTPPort = deser._read_le_uint16() # platformHTTPPort - payloadSig = deser._read_nbytes(96) # payloadSig - return DashProUpServTx( - version=version, - type=ntype, - proTxHash=proTxHash, - ipAddress=ipAddress, - port=port, - scriptOperatorPayout=scriptOperatorPayout, - inputsHash=inputsHash, - platformNodeID=platformNodeID, - platformP2PPort=platformP2PPort, - platformHTTPPort=platformHTTPPort, - payloadSig=payloadSig, - ) - - -@dataclass(kw_only=True, slots=True) -class DashProUpRegTx: - '''Class representing DIP3 ProUpRegTx''' - version: int - proTxHash: bytes - mode: int - PubKeyOperator: bytes - KeyIdVoting: bytes - scriptPayout: bytes - inputsHash: bytes - payloadSig: bytes - - def serialize(self): - assert (len(self.proTxHash) == 32 - and len(self.PubKeyOperator) == 48 - and len(self.KeyIdVoting) == 20 - and len(self.inputsHash) == 32) - return ( - pack_le_uint16(self.version) + # version - self.proTxHash + # proTxHash - pack_le_uint16(self.mode) + # mode - self.PubKeyOperator + # PubKeyOperator - self.KeyIdVoting + # KeyIdVoting - pack_varbytes(self.scriptPayout) + # scriptPayout - self.inputsHash + # inputsHash - pack_varbytes(self.payloadSig) # payloadSig - ) - - @classmethod - def read_tx_extra(cls, deser): - return DashProUpRegTx( - version=deser._read_le_uint16(), - proTxHash=deser._read_nbytes(32), - mode=deser._read_le_uint16(), - PubKeyOperator=deser._read_nbytes(48), - KeyIdVoting=deser._read_nbytes(20), - scriptPayout=deser._read_varbytes(), - inputsHash=deser._read_nbytes(32), - payloadSig=deser._read_varbytes(), - ) - - -@dataclass(kw_only=True, slots=True) -class DashProUpRevTx: - '''Class representing DIP3 ProUpRevTx''' - version: int - proTxHash: bytes - reason: int - inputsHash: bytes - payloadSig: bytes - - def serialize(self): - assert (len(self.proTxHash) == 32 - and len(self.inputsHash) == 32 - and len(self.payloadSig) == 96) - return ( - pack_le_uint16(self.version) + # version - self.proTxHash + # proTxHash - pack_le_uint16(self.reason) + # reason - self.inputsHash + # inputsHash - self.payloadSig # payloadSig - ) - - @classmethod - def read_tx_extra(cls, deser): - return DashProUpRevTx( - version=deser._read_le_uint16(), - proTxHash=deser._read_nbytes(32), - reason=deser._read_le_uint16(), - inputsHash=deser._read_nbytes(32), - payloadSig=deser._read_nbytes(96), - ) - - -@dataclass(kw_only=True, slots=True) -class DashCbTx: - '''Class representing DIP4 coinbase special tx''' - version: int - height: int - merkleRootMNList: bytes - merkleRootQuorums: bytes - bestCLHeightDiff: int - bestCLSignature: bytes - assetLockedAmount: int - - def serialize(self): - assert len(self.merkleRootMNList) == 32 - res = ( - pack_le_uint16(self.version) + # version - pack_le_uint32(self.height) + # height - self.merkleRootMNList # merkleRootMNList - ) - if self.version > 1: - assert len(self.merkleRootQuorums) == 32 - res += self.merkleRootQuorums # merkleRootQuorums - if self.version > 2: - res += pack_varint(self.bestCLHeightDiff) - res += self.bestCLSignature - res += pack_le_int64(self.assetLockedAmount) - return res - - @classmethod - def read_tx_extra(cls, deser): - version = deser._read_le_uint16() - height = deser._read_le_uint32() - merkleRootMNList = deser._read_nbytes(32) - merkleRootQuorums = b'' - bestCLHeightDiff = 0 - bestCLSignature = b'' - assetLockedAmount = 0 - if version > 1: - merkleRootQuorums = deser._read_nbytes(32) - if version > 2: - bestCLHeightDiff = deser._read_varint() - bestCLSignature = deser._read_nbytes(96) - assetLockedAmount = deser._read_le_uint64() - return DashCbTx( - version=version, - height=height, - merkleRootMNList=merkleRootMNList, - merkleRootQuorums=merkleRootQuorums, - bestCLHeightDiff=bestCLHeightDiff, - bestCLSignature=bestCLSignature, - assetLockedAmount=assetLockedAmount, - ) - - -# https://dash-docs.github.io/en/developer-reference#outpoint -@dataclass(kw_only=True, slots=True) -class TxOutPoint: - '''Class representing tx output outpoint''' - hash: bytes - index: int - - def serialize(self): - assert len(self.hash) == 32 - return ( - self.hash + # hash - pack_le_uint32(self.index) # index - ) - - @classmethod - def read_outpoint(cls, deser): - return TxOutPoint( - hash=deser._read_nbytes(32), - index=deser._read_le_uint32(), - ) - - -class DeserializerDash(Deserializer): - '''Deserializer for Dash DIP2 special tx types''' - # Supported Spec Tx types and corresponding classes mapping - PRO_REG_TX = 1 - PRO_UP_SERV_TX = 2 - PRO_UP_REG_TX = 3 - PRO_UP_REV_TX = 4 - CB_TX = 5 - - SPEC_TX_HANDLERS = { - PRO_REG_TX: DashProRegTx, - PRO_UP_SERV_TX: DashProUpServTx, - PRO_UP_REG_TX: DashProUpRegTx, - PRO_UP_REV_TX: DashProUpRevTx, - CB_TX: DashCbTx, - } - - def _read_outpoint(self): - return TxOutPoint.read_outpoint(self) - - def read_tx(self): - start = self.cursor - header = self._read_le_uint32() - tx_type = header >> 16 # DIP2 tx type - if tx_type: - version = header & 0x0000ffff - else: - version = header - - if tx_type and version < 3: - version = header - tx_type = 0 - - inputs = self._read_inputs() - outputs = self._read_outputs() - locktime = self._read_le_uint32() - if tx_type: - extra_payload_size = self._read_varint() - end = self.cursor + extra_payload_size - spec_tx_class = DeserializerDash.SPEC_TX_HANDLERS.get(tx_type) - if spec_tx_class: - read_method = getattr(spec_tx_class, 'read_tx_extra', None) - extra_payload = read_method(self) - assert isinstance(extra_payload, spec_tx_class) - else: - extra_payload = self._read_nbytes(extra_payload_size) - assert self.cursor == end - else: - extra_payload = b'' - txid = self.TX_HASH_FN(self.binary[start:self.cursor]) - tx = DashTx( - version=version, - inputs=inputs, - outputs=outputs, - locktime=locktime, - tx_type=tx_type, - extra_payload=extra_payload, - txid_rev=txid, - wtxid_rev=txid, - ) - return tx diff --git a/src/electrumx/server/block_processor.py b/src/electrumx/server/block_processor.py index 8c3c19b..8a2169e 100644 --- a/src/electrumx/server/block_processor.py +++ b/src/electrumx/server/block_processor.py @@ -904,107 +904,3 @@ class BlockProcessor: self.blocks_event.set() return True return False - - -class DecredBlockProcessor(BlockProcessor): - async def calc_reorg_range(self, count): - start, count = await super().calc_reorg_range(count) - if start > 0: - # A reorg in Decred can invalidate the previous block - start -= 1 - count += 1 - return start, count - - -class NameIndexBlockProcessor(BlockProcessor): - - def advance_txs_process_outputs( - self, - txs: Sequence[Tx], - *, - height: int, - ) -> None: - result = super().advance_txs_process_outputs(txs, height=height) - - tx_num_start = self.db.tx_counts[height - 1] if height > 0 else 0 - tx_num = tx_num_start - script_name_hashX = self.coin.name_hashX_from_script - update_touched_hashxs = self.touched_hashxs.update - hashXs_by_tx = [] - append_hashXs = hashXs_by_tx.append - - for tx in txs: - hashXs = [] - append_hashX = hashXs.append - - # Add the new UTXOs and associate them with the name script - for txout in tx.outputs: - # Get the hashX of the name script. Ignore non-name scripts. - hashX = script_name_hashX(txout.pk_script) - if hashX: - append_hashX(hashX) - - append_hashXs(hashXs) - update_touched_hashxs(hashXs) - tx_num += 1 - - self.db.history.add_unflushed(hashXs_by_tx, tx_num_start) - - return result - - -class LTORBlockProcessor(BlockProcessor): - # for Lexicographical Transaction ordering in blocks - # (as opposed to Topological order as used in Bitcoin) - # note: the base class already does not care about tx ordering inside a block - # for the forward (catch-up) direction, as that better parallelizes. - # This subclass is only kept for the reverse (backup) direction, for now. - # We could also parallelize backup_txs in the base class for better perf, - # likely removing the tx ordering assumptions, then we could rm this subclass. - - def backup_txs(self, txs, is_unspendable): - undo_info = self.db.read_undo_info(self.height) - if undo_info is None: - raise ChainError( - f'no undo information found for height {self.height:,d}' - ) - - # Use local vars for speed in the loops - put_utxo = self.utxo_cache.__setitem__ - spend_utxo = self.spend_utxo - add_touched_hashx = self.touched_hashxs.add - add_touched_outpoint = self.touched_outpoints.add - undo_entry_len = HASHX_LEN + TXNUM_LEN + 8 - - # Restore coins that had been spent - # (may include coins made then spent in this block) - n = 0 - for tx in txs: - for txin in tx.inputs: - if txin.is_generation(): - continue - undo_item = undo_info[n:n + undo_entry_len] - prevout = txin.prev_txid_rev + pack_txoutidx(txin.prev_idx) - put_utxo(prevout, undo_item) - add_touched_hashx(undo_item[:HASHX_LEN]) - add_touched_outpoint((txin.prev_txid_rev, txin.prev_idx)) - n += undo_entry_len - - assert n == len(undo_info) - - # Remove tx outputs made in this block, by spending them. - for tx in txs: - txid_rev = tx.txid_rev - for idx, txout in enumerate(tx.outputs): - # Spend the TX outputs. Be careful with unspendable - # outputs - we didn't save those in the first place. - if is_unspendable(txout.pk_script): - continue - - # Get the hashX - cache_value = spend_utxo(txid_rev, idx) - hashX = cache_value[:HASHX_LEN] - add_touched_hashx(hashX) - add_touched_outpoint((txid_rev, idx)) - - self.tx_count -= len(txs) diff --git a/src/electrumx/server/daemon.py b/src/electrumx/server/daemon.py index e02be5c..d093c08 100644 --- a/src/electrumx/server/daemon.py +++ b/src/electrumx/server/daemon.py @@ -11,18 +11,13 @@ daemon.''' import asyncio import itertools import time -from calendar import timegm -from struct import pack from typing import TYPE_CHECKING, Type, Sequence, Any, Iterable, Optional import aiohttp from aiorpcx import JSONRPC -from electrumx.lib.hash import hash_to_hex_str, hex_str_to_hash -from electrumx.lib.tx import DeserializerDecred from electrumx.lib.util import (class_logger, hex_to_bytes, json_deserialize, - json_serialize, pack_varint, - unpack_le_uint16_from) + json_serialize) if TYPE_CHECKING: from electrumx.lib.coins import Coin @@ -404,235 +399,3 @@ class Daemon: If the daemon has not been queried yet this returns None.''' return self._height - - -class DashDaemon(Daemon): - - async def masternode_broadcast(self, params): - '''Broadcast a transaction to the network.''' - return await self._send_single('masternodebroadcast', params) - - async def masternode_list(self, params): - '''Return the masternode status.''' - return await self._send_single('masternodelist', params) - - async def protx(self, params): - '''Set of commands to execute ProTx related actions.''' - return await self._send_single('protx', params) - - -class FakeEstimateFeeDaemon(Daemon): - '''Daemon that simulates estimatefee and relayfee RPC calls. Coin that - wants to use this daemon must define ESTIMATE_FEE & RELAY_FEE''' - - async def estimatefee(self, block_count): - '''Return the fee estimate for the given parameters.''' - return self.coin.ESTIMATE_FEE - - async def relayfee(self): - """The minimum fee required for a transaction to be relayed on by the server to the - bitcoin network. Doesn't guarantee mempool acceptance.""" - return self.coin.RELAY_FEE - - -class LegacyRPCDaemon(Daemon): - '''Handles connections to a daemon at the given URL. - - This class is useful for daemons that don't have the new 'getblock' - RPC call that returns the block in hex, the workaround is to manually - recreate the block bytes. The recreated block bytes may not be the exact - as in the underlying blockchain but it is good enough for our indexing - purposes.''' - - async def raw_blocks(self, bhashes_hum): - '''Return the raw binary blocks with the given hex hashes.''' - params_iterable = ((h, ) for h in bhashes_hum) - block_info = await self._send_vector('getblock', params_iterable) - - blocks = [] - for i in block_info: - raw_block = await self.make_raw_block(i) - blocks.append(raw_block) - - # Convert hex string to bytes - return blocks - - async def make_raw_header(self, b): - pbh = b.get('previousblockhash') - if pbh is None: - pbh = '0' * 64 - return b''.join([ - pack(' 0: - transactions = await self.getrawtransactions(b.get('tx'), False) - - raw_block = header - num_txs = len(transactions) - if num_txs > 0: - raw_block += pack_varint(num_txs) - raw_block += b''.join(transactions) - else: - raw_block += b'\x00' - - return raw_block - - def timestamp_safe(self, t): - if isinstance(t, int): - return t - return timegm(time.strptime(t, "%Y-%m-%d %H:%M:%S %Z")) - - -class FakeEstimateLegacyRPCDaemon(LegacyRPCDaemon, FakeEstimateFeeDaemon): - pass - - -class DecredDaemon(Daemon): - async def raw_blocks(self, bhashes_hum): - '''Return the raw binary blocks with the given hex hashes.''' - - params_iterable = ((h, False) for h in bhashes_hum) - 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_le_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(bhashes_hum, 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_txids_hum(self): - mempool = await super().mempool_txids_hum() - # 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 connector(self): - # FIXME allow self signed certificates - return aiohttp.TCPConnector(verify_ssl=False) - - -class PreLegacyRPCDaemon(LegacyRPCDaemon): - '''Handles connections to a daemon at the given URL. - - This class is useful for daemons that don't have the new 'getblock' - RPC call that returns the block in hex, and need the False parameter - for the getblock''' - - async def deserialised_block(self, hex_hash): - '''Return the deserialised block with the given hex hash.''' - return await self._send_single('getblock', (hex_hash, False)) - - -class SmartCashDaemon(Daemon): - - async def masternode_broadcast(self, params): - '''Broadcast a smartnode to the network.''' - return await self._send_single('smartnodebroadcast', params) - - async def masternode_list(self, params): - '''Return the smartnode status.''' - return await self._send_single('smartnodelist', params) - - async def smartrewards(self, params): - '''Return smartrewards data.''' - return await self._send_single('smartrewards', params) - - -class ZcoinMtpDaemon(Daemon): - - def strip_mtp_data(self, raw_block): - if self.coin.is_mtp(raw_block): - return \ - raw_block[:self.coin.MTP_HEADER_DATA_START*2] + \ - raw_block[self.coin.MTP_HEADER_DATA_END*2:] - return raw_block - - async def raw_blocks(self, bhashes_hum): - '''Return the raw binary blocks with the given hex hashes.''' - params_iterable = ((h, False) for h in bhashes_hum) - blocks = await self._send_vector('getblock', params_iterable) - # Convert hex string to bytes - return [hex_to_bytes(self.strip_mtp_data(block)) for block in blocks] - - async def masternode_broadcast(self, params): - '''Broadcast a transaction to the network.''' - return await self._send_single('znodebroadcast', params) - - async def masternode_list(self, params): - '''Return the masternode status.''' - return await self._send_single('znodelist', params) - - async def protx(self, params): - '''Set of commands to execute ProTx related actions.''' - return await self._send_single('protx', params) diff --git a/src/electrumx/server/session.py b/src/electrumx/server/session.py index 6de279d..e0c68ac 100644 --- a/src/electrumx/server/session.py +++ b/src/electrumx/server/session.py @@ -10,7 +10,6 @@ import asyncio import codecs from dataclasses import dataclass -import datetime import itertools import math import os @@ -2439,335 +2438,3 @@ class LocalRPC(SessionBase): def protocol_version_string(self): return 'RPC' - - -###################################################################### -# Non-Bitcoin stuff goes strictly below this line. -###################################################################### - - -class DashElectrumX(ElectrumX): - '''A TCP server that handles incoming Electrum Dash connections.''' - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.mns = set() - self.mn_cache_height = 0 - self.mn_cache = [] - - def set_request_handlers(self, ptuple): - super().set_request_handlers(ptuple) - self.request_handlers.update({ - 'masternode.announce.broadcast': self.phandle_masternode_announce_broadcast, - 'masternode.subscribe': self.phandle_masternode_subscribe, - 'masternode.list': self.phandle_masternode_list, - 'protx.diff': self.phandle_protx_diff, - 'protx.info': self.phandle_protx_info, - }) - - async def _notify_inner( - self, - *, - touched_hashxs, - touched_outpoints, - height_changed, - ): - '''Notify the client about changes in masternode list.''' - await super()._notify_inner( - touched_hashxs=touched_hashxs, - touched_outpoints=touched_outpoints, - height_changed=height_changed, - ) - for mn in self.mns.copy(): - status = await self.daemon_request('masternode_list', - ('status', mn)) - await self.send_notification('masternode.subscribe', - (mn, status.get(mn))) - - # Masternode command handlers - async def phandle_masternode_announce_broadcast(self, signmnb): - '''Pass through the masternode announce message to be broadcast - by the daemon. - - signmnb: signed masternode broadcast message.''' - try: - return await self.daemon_request('masternode_broadcast', - ('relay', signmnb)) - except DaemonError as e: - error, = e.args - message = error['message'] - self.logger.info(f'masternode_broadcast: {message}') - raise RPCError(BAD_REQUEST, 'the masternode broadcast was ' - f'rejected.\n\n{message}\n[{signmnb}]') - - async def phandle_masternode_subscribe(self, collateral): - '''Returns the status of masternode. - - collateral: masternode collateral. - ''' - result = await self.daemon_request('masternode_list', - ('status', collateral)) - if result is not None: - self.mns.add(collateral) - return result.get(collateral) - return None - - async def phandle_masternode_list(self, payees): - ''' - Returns the list of masternodes. - - payees: a list of masternode payee addresses. - ''' - if not isinstance(payees, list): - raise RPCError(BAD_REQUEST, 'expected a list of payees') - - def get_masternode_payment_queue(mns): - '''Returns the calculated position in the payment queue for all the - valid masterernodes in the given mns list. - - mns: a list of masternodes information. - ''' - now = int(datetime.datetime.utcnow().strftime("%s")) - mn_queue = [] - - # Only ENABLED masternodes are considered for the list. - for line in mns: - mnstat = mns[line].split() - if mnstat[0] == 'ENABLED': - # if last paid time == 0 - if int(mnstat[5]) == 0: - # use active seconds - mnstat.append(int(mnstat[4])) - else: - # now minus last paid - delta = now - int(mnstat[5]) - # if > active seconds, use active seconds - if delta >= int(mnstat[4]): - mnstat.append(int(mnstat[4])) - # use active seconds - else: - mnstat.append(delta) - mn_queue.append(mnstat) - mn_queue = sorted(mn_queue, key=lambda x: x[8], reverse=True) - return mn_queue - - def get_payment_position(payment_queue, address): - ''' - Returns the position of the payment list for the given address. - - payment_queue: position in the payment queue for the masternode. - address: masternode payee address. - ''' - position = -1 - for pos, mn in enumerate(payment_queue, start=1): - if mn[2] == address: - position = pos - break - return position - - # Accordingly with the masternode payment queue, a custom list - # with the masternode information including the payment - # position is returned. - cache = self.session_mgr.mn_cache - if not cache or self.session_mgr.mn_cache_height != self.db.db_height: - full_mn_list = await self.daemon_request('masternode_list', - ('full',)) - mn_payment_queue = get_masternode_payment_queue(full_mn_list) - mn_payment_count = len(mn_payment_queue) - mn_list = [] - for key, value in full_mn_list.items(): - mn_data = value.split() - mn_info = { - 'vin': key, - 'status': mn_data[0], - 'protocol': mn_data[1], - 'payee': mn_data[2], - 'lastseen': mn_data[3], - 'activeseconds': mn_data[4], - 'lastpaidtime': mn_data[5], - 'lastpaidblock': mn_data[6], - 'ip': mn_data[7] - } - mn_info['paymentposition'] = get_payment_position( - mn_payment_queue, mn_info['payee'] - ) - mn_info['inselection'] = ( - mn_info['paymentposition'] < mn_payment_count // 10 - ) - hashX = self.coin.address_to_hashX(mn_info['payee']) - balance = await self.get_balance(hashX) - mn_info['balance'] = (sum(balance.values()) - / self.coin.VALUE_PER_COIN) - mn_list.append(mn_info) - cache.clear() - cache.extend(mn_list) - self.session_mgr.mn_cache_height = self.db.db_height - - # If payees is an empty list the whole masternode list is returned - if payees: - return [mn for mn in cache if mn['payee'] in payees] - else: - return cache - - async def phandle_protx_diff(self, base_height, height): - ''' - Calculates a diff between two deterministic masternode lists. - The result also contains proof data. - - base_height: The starting block height (starting from 1). - height: The ending block height. - ''' - if not isinstance(base_height, int) or not isinstance(height, int): - raise RPCError(BAD_REQUEST, 'expected a int block heights') - - max_height = self.db.db_height - if (not 1 <= base_height <= max_height or - not base_height <= height <= max_height): - raise RPCError(BAD_REQUEST, - f'require 1 <= base_height {base_height:,d} <= ' - f'height {height:,d} <= ' - f'chain height {max_height:,d}') - - return await self.daemon_request('protx', - ('diff', base_height, height)) - - async def phandle_protx_info(self, protx_hash): - ''' - Returns detailed information about a deterministic masternode. - - protx_hash: The hash of the initial ProRegTx - ''' - if not isinstance(protx_hash, str): - raise RPCError(BAD_REQUEST, 'expected protx hash string') - - res = await self.daemon_request('protx', ('info', protx_hash)) - if 'wallet' in res: - del res['wallet'] - return res - - -class SmartCashElectrumX(DashElectrumX): - '''A TCP server that handles incoming Electrum-SMART connections.''' - - def set_request_handlers(self, ptuple): - super().set_request_handlers(ptuple) - self.request_handlers.update({ - 'smartrewards.current': self.phandle_smartrewards_current, - 'smartrewards.check': self.phandle_smartrewards_check - }) - - async def phandle_smartrewards_current(self): - '''Returns the current smartrewards info.''' - result = await self.daemon_request('smartrewards', ('current',)) - if result is not None: - return result - return None - - async def phandle_smartrewards_check(self, addr): - ''' - Returns the status of an address - - addr: a single smartcash address - ''' - result = await self.daemon_request('smartrewards', ('check', addr)) - if result is not None: - return result - return None - - -class AuxPoWElectrumX(ElectrumX): - async def phandle_block_header(self, height, cp_height=0): - result = await super().phandle_block_header(height, cp_height) - - # Older protocol versions don't truncate AuxPoW - if self.protocol_tuple < (1, 4, 1): - return result - - # Not covered by a checkpoint; return full AuxPoW data - if cp_height == 0: - return result - - # Covered by a checkpoint; truncate AuxPoW data - result['header'] = self.truncate_auxpow_single(result['header']) - return result - - async def phandle_block_headers(self, start_height, count, cp_height=0): - # Older protocol versions don't truncate AuxPoW - if self.protocol_tuple < (1, 4, 1): - return await super().phandle_block_headers(start_height, count, cp_height) - - # Not covered by a checkpoint; return full AuxPoW data - if cp_height == 0: - return await super().phandle_block_headers(start_height, count, cp_height) - - result = await super().block_headers_array(start_height, count, cp_height) - - # Covered by a checkpoint; truncate AuxPoW data - result['headers'] = self.truncate_auxpow_headers(result['headers']) - - # Return headers in array form - if self.protocol_tuple >= (1, 6): - return result - - # Return headers in concatenated form - result['hex'] = ''.join(result['headers']) - del result['headers'] - return result - - def truncate_auxpow_headers(self, headers): - result = [] - for header in headers: - result.append(self.truncate_auxpow_single(header)) - return result - - def truncate_auxpow_single(self, header: str): - # 2 hex chars per byte - return header[:2*self.coin.TRUNCATED_HEADER_SIZE] - - -class NameIndexElectrumX(ElectrumX): - def set_request_handlers(self, ptuple): - super().set_request_handlers(ptuple) - - if ptuple >= (1, 4, 3): - self.request_handlers['blockchain.name.get_value_proof'] = self.phandle_name_get_value_proof - - async def phandle_name_get_value_proof(self, scripthash, cp_height=0): - history = await self.phandle_scripthash_get_history(scripthash) - - trimmed_history = [] - prev_height = None - - for update in history[::-1]: - txid = update['tx_hash'] - height = update['height'] - - if (self.coin.NAME_EXPIRATION is not None - and prev_height is not None - and height < prev_height - self.coin.NAME_EXPIRATION): - break - - tx = await self.phandle_transaction_get(txid) - update['tx'] = tx - del update['tx_hash'] - - tx_merkle = await self.phandle_transaction_merkle(txid, height) - del tx_merkle['block_height'] - update['tx_merkle'] = tx_merkle - - if height <= cp_height: - header = await self.phandle_block_header(height, cp_height) - update['header'] = header - - trimmed_history.append(update) - - if height <= cp_height: - break - - prev_height = height - - return {scripthash: trimmed_history} - - -class NameIndexAuxPoWElectrumX(NameIndexElectrumX, AuxPoWElectrumX): - pass