|
@@ -14,10 +14,16 @@ proto.btc.tx.base: Bitcoin base transaction class
|
|
|
|
|
|
|
|
from collections import namedtuple
|
|
from collections import namedtuple
|
|
|
|
|
|
|
|
|
|
+from ....addr import CoinAddr
|
|
|
from ....tx import base as TxBase
|
|
from ....tx import base as TxBase
|
|
|
-from ....obj import MMGenList, HexStr
|
|
|
|
|
|
|
+from ....obj import MMGenList, HexStr, ListItemAttr
|
|
|
from ....util import msg, make_chksum_6, die, pp_fmt
|
|
from ....util import msg, make_chksum_6, die, pp_fmt
|
|
|
|
|
|
|
|
|
|
+from .op_return_data import OpReturnData
|
|
|
|
|
+
|
|
|
|
|
+def data2scriptPubKey(data):
|
|
|
|
|
+ return '6a' + '{:02x}'.format(len(data)) + data.hex() # OP_RETURN data
|
|
|
|
|
+
|
|
|
def addr2scriptPubKey(proto, addr):
|
|
def addr2scriptPubKey(proto, addr):
|
|
|
|
|
|
|
|
def decode_addr(proto, addr):
|
|
def decode_addr(proto, addr):
|
|
@@ -31,15 +37,31 @@ def addr2scriptPubKey(proto, addr):
|
|
|
'bech32': proto.witness_vernum_hex + '14' + decode_addr(proto, addr)
|
|
'bech32': proto.witness_vernum_hex + '14' + decode_addr(proto, addr)
|
|
|
}[addr.addr_fmt]
|
|
}[addr.addr_fmt]
|
|
|
|
|
|
|
|
-def scriptPubKey2addr(proto, s):
|
|
|
|
|
|
|
+def decodeScriptPubKey(proto, s):
|
|
|
|
|
+ # src/wallet/rpc/addresses.cpp:
|
|
|
|
|
+ # types: nonstandard, pubkey, pubkeyhash, scripthash, multisig, nulldata, witness_v0_keyhash
|
|
|
|
|
+ ret = namedtuple('decoded_scriptPubKey', ['type', 'addr_fmt', 'addr', 'data'])
|
|
|
|
|
+
|
|
|
if len(s) == 50 and s[:6] == '76a914' and s[-4:] == '88ac':
|
|
if len(s) == 50 and s[:6] == '76a914' and s[-4:] == '88ac':
|
|
|
- return proto.pubhash2addr(bytes.fromhex(s[6:-4]), 'p2pkh'), 'p2pkh'
|
|
|
|
|
|
|
+ return ret('pubkeyhash', 'p2pkh', proto.pubhash2addr(bytes.fromhex(s[6:-4]), 'p2pkh'), None)
|
|
|
|
|
+
|
|
|
elif len(s) == 46 and s[:4] == 'a914' and s[-2:] == '87':
|
|
elif len(s) == 46 and s[:4] == 'a914' and s[-2:] == '87':
|
|
|
- return proto.pubhash2addr(bytes.fromhex(s[4:-2]), 'p2sh'), 'p2sh'
|
|
|
|
|
|
|
+ return ret('scripthash', 'p2sh', proto.pubhash2addr(bytes.fromhex(s[4:-2]), 'p2sh'), None)
|
|
|
|
|
+
|
|
|
elif len(s) == 44 and s[:4] == proto.witness_vernum_hex + '14':
|
|
elif len(s) == 44 and s[:4] == proto.witness_vernum_hex + '14':
|
|
|
- return proto.pubhash2bech32addr(bytes.fromhex(s[4:])), 'bech32'
|
|
|
|
|
|
|
+ return ret('witness_v0_keyhash', 'bech32', proto.pubhash2bech32addr(bytes.fromhex(s[4:])), None)
|
|
|
|
|
+
|
|
|
|
|
+ elif s[:2] == '6a': # OP_RETURN
|
|
|
|
|
+ # range 1-80 == hex 2-160, plus 4 for opcode byte + push byte
|
|
|
|
|
+ if 6 <= len(s) <= (proto.max_op_return_data_len * 2) + 6: # 2-160 -> 6-166
|
|
|
|
|
+ return ret('nulldata', None, None, s[4:]) # return data in hex format
|
|
|
|
|
+ else:
|
|
|
|
|
+ raise ValueError('{}: OP_RETURN data bytes length not in range 1-{}'.format(
|
|
|
|
|
+ len(s[4:]) // 2,
|
|
|
|
|
+ proto.max_op_return_data_len))
|
|
|
|
|
+
|
|
|
else:
|
|
else:
|
|
|
- raise NotImplementedError(f'Unknown scriptPubKey ({s})')
|
|
|
|
|
|
|
+ raise NotImplementedError(f'Unrecognized scriptPubKey ({s})')
|
|
|
|
|
|
|
|
def DeserializeTX(proto, txhex):
|
|
def DeserializeTX(proto, txhex):
|
|
|
"""
|
|
"""
|
|
@@ -119,7 +141,7 @@ def DeserializeTX(proto, txhex):
|
|
|
} for i in range(d['num_txouts'])])
|
|
} for i in range(d['num_txouts'])])
|
|
|
|
|
|
|
|
for o in d['txouts']:
|
|
for o in d['txouts']:
|
|
|
- o['address'] = scriptPubKey2addr(proto, o['scriptPubKey'])[0]
|
|
|
|
|
|
|
+ o.update(decodeScriptPubKey(proto, o['scriptPubKey'])._asdict())
|
|
|
|
|
|
|
|
if has_witness:
|
|
if has_witness:
|
|
|
# https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
|
|
# https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
|
|
@@ -152,6 +174,10 @@ class Base(TxBase.Base):
|
|
|
rel_fee_disp = 'sat/byte'
|
|
rel_fee_disp = 'sat/byte'
|
|
|
_deserialized = None
|
|
_deserialized = None
|
|
|
|
|
|
|
|
|
|
+ class Output(TxBase.Base.Output): # output contains either addr or data, but not both
|
|
|
|
|
+ addr = ListItemAttr(CoinAddr, include_proto=True) # ImmutableAttr in parent cls
|
|
|
|
|
+ data = ListItemAttr(OpReturnData, include_proto=True, typeconv=True) # type None in parent cls
|
|
|
|
|
+
|
|
|
class InputList(TxBase.Base.InputList):
|
|
class InputList(TxBase.Base.InputList):
|
|
|
|
|
|
|
|
# Lexicographical Indexing of Transaction Inputs and Outputs
|
|
# Lexicographical Indexing of Transaction Inputs and Outputs
|
|
@@ -169,7 +195,9 @@ class Base(TxBase.Base):
|
|
|
def sort_func(a):
|
|
def sort_func(a):
|
|
|
return (
|
|
return (
|
|
|
int.to_bytes(a.amt.to_unit('satoshi'), 8, 'big')
|
|
int.to_bytes(a.amt.to_unit('satoshi'), 8, 'big')
|
|
|
- + bytes.fromhex(addr2scriptPubKey(self.parent.proto, a.addr)))
|
|
|
|
|
|
|
+ + bytes.fromhex(
|
|
|
|
|
+ addr2scriptPubKey(self.parent.proto, a.addr) if a.addr else
|
|
|
|
|
+ data2scriptPubKey(a.data)))
|
|
|
self.sort(key=sort_func)
|
|
self.sort(key=sort_func)
|
|
|
|
|
|
|
|
def has_segwit_inputs(self):
|
|
def has_segwit_inputs(self):
|
|
@@ -215,9 +243,15 @@ class Base(TxBase.Base):
|
|
|
return ret + sum(input_size['C'] for i in self.inputs if not i.mmtype)
|
|
return ret + sum(input_size['C'] for i in self.inputs if not i.mmtype)
|
|
|
|
|
|
|
|
def get_outputs_size():
|
|
def get_outputs_size():
|
|
|
- # output bytes = amt: 8, byte_count: 1+, pk_script
|
|
|
|
|
- # pk_script bytes: p2pkh: 25, p2sh: 23, bech32: 22
|
|
|
|
|
- return sum({'p2pkh':34, 'p2sh':32, 'bech32':31}[o.addr.addr_fmt] for o in self.outputs)
|
|
|
|
|
|
|
+ # output bytes:
|
|
|
|
|
+ # 8 (amt) + scriptlen_byte + script_bytes
|
|
|
|
|
+ # script_bytes:
|
|
|
|
|
+ # ADDR: p2pkh: 25, p2sh: 23, bech32: 22
|
|
|
|
|
+ # DATA: opcode_byte ('6a') + push_byte + nulldata_bytes
|
|
|
|
|
+ return sum(
|
|
|
|
|
+ {'p2pkh':34, 'p2sh':32, 'bech32':31}[o.addr.addr_fmt] if o.addr else
|
|
|
|
|
+ (11 + len(o.data))
|
|
|
|
|
+ for o in self.outputs)
|
|
|
|
|
|
|
|
# https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
|
|
# https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
|
|
|
# The witness is a serialization of all witness data of the transaction. Each txin is
|
|
# The witness is a serialization of all witness data of the transaction. Each txin is
|
|
@@ -317,8 +351,8 @@ class Base(TxBase.Base):
|
|
|
|
|
|
|
|
check_equal(
|
|
check_equal(
|
|
|
'outputs',
|
|
'outputs',
|
|
|
- sorted((o['address'], o['amt']) for o in dtx.txouts),
|
|
|
|
|
- sorted((o.addr, o.amt) for o in self.outputs))
|
|
|
|
|
|
|
+ sorted((o['addr'] or o['data'], o['amt']) for o in dtx.txouts),
|
|
|
|
|
+ sorted((o.addr or o.data.hex(), o.amt) for o in self.outputs))
|
|
|
|
|
|
|
|
if str(self.txid) != make_chksum_6(bytes.fromhex(dtx.unsigned_hex)).upper():
|
|
if str(self.txid) != make_chksum_6(bytes.fromhex(dtx.unsigned_hex)).upper():
|
|
|
do_error(f'MMGen TxID ({self.txid}) does not match serialized transaction data!')
|
|
do_error(f'MMGen TxID ({self.txid}) does not match serialized transaction data!')
|