btc.tx.base.decodeScriptPubKey(): reimplement, parse nulldata correctly

This commit is contained in:
The MMGen Project 2025-09-29 23:09:17 +00:00
commit 4b55f1158e
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
5 changed files with 54 additions and 23 deletions

View file

@ -41,26 +41,40 @@ def decodeScriptPubKey(proto, s):
# 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':
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':
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':
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,
match len(s):
case 50 if s.startswith('76a914') and s.endswith('88ac'):
return ret('pubkeyhash', 'p2pkh', proto.pubhash2addr(bytes.fromhex(s[6:-4]), 'p2pkh'), None)
case 46 if s.startswith('a914') and s.endswith('87'):
return ret('scripthash', 'p2sh', proto.pubhash2addr(bytes.fromhex(s[4:-2]), 'p2sh'), None)
case 44 if s.startswith(proto.witness_vernum_hex + '14'):
return ret(
'witness_v0_keyhash',
'bech32',
proto.pubhash2bech32addr(bytes.fromhex(s[4:])),
None)
case 2 if s.startswith('6a'): # bare OP_RETURN
return ret('nulldata', None, None, '')
case x if s.startswith('6a'): # OP_RETURN with data
# skip opcode byte + push byte(s): https://en.bitcoin.it/wiki/Script
match int(s[2:4], 16):
case y if 0 < y < 76:
skip = 2
case 76:
skip = 3
case 77:
skip = 4
case 78:
skip = 6
case y:
raise ValueError(f'{y}: invalid first push byte in OP_RETURN data')
if 1 <= (x >> 1) - skip <= proto.max_op_return_data_len:
return ret('nulldata', None, None, s[skip * 2:]) # return data in hex format
else:
raise ValueError('{}: OP_RETURN data bytes length not in range 1-{}'.format(
(x >> 1) - skip,
proto.max_op_return_data_len))
else:
raise NotImplementedError(f'Unrecognized scriptPubKey ({s})')
case _:
raise NotImplementedError(f'Unrecognized scriptPubKey ({s})')
def DeserializeTX(proto, txhex):
"""

View file

@ -415,7 +415,9 @@ class CmdTestMain(CmdTestBase, CmdTestShared):
return self.addrgen(wf=None, dfl_wallet=True)
def txcreate_dfl_wallet(self, addrfile):
return self.txcreate_common(sources=['15'])
return self.txcreate_common(
sources = ['15'],
add_output_args = ['data:' + 'z' * self.proto.max_op_return_data_len])
def txsign_dfl_wallet(self, txfile, pf='', save=True, has_label=False):
return self.txsign(None, txfile, save=save, has_label=has_label, dfl_wallet=True)
@ -688,6 +690,7 @@ class CmdTestMain(CmdTestBase, CmdTestShared):
do_label = False,
ss_args = [],
add_opts = [],
add_output_args = [],
view = 'n',
addrs_per_wallet = addrs_per_wallet,
non_mmgen_input_compressed = True,
@ -725,6 +728,7 @@ class CmdTestMain(CmdTestBase, CmdTestShared):
+ add_opts
+ (make_input_opts() if cmdline_inputs else [])
+ self._make_txcreate_outputs(tx_data)
+ add_output_args
+ [tx_data[num]['addrfile'] for num in tx_data]
+ ss_args)
@ -765,7 +769,10 @@ class CmdTestMain(CmdTestBase, CmdTestShared):
return t
def txcreate(self, addrfile):
return self.txcreate_common(sources=['1'], add_opts=['--vsize-adj=1.01'])
return self.txcreate_common(
sources = ['1'],
add_opts = ['--vsize-adj=1.01'],
add_output_args = ['hexdata:' + 'ee' * self.proto.max_op_return_data_len])
def txbump(self, txfile, prepend_args=[], seed_args=[]):
if not self.proto.cap('rbf'):

View file

@ -34,7 +34,7 @@ async def test_tx(tx_proto, tx_hex, desc, n):
def has_nonstandard_outputs(outputs):
for o in outputs:
t = o['scriptPubKey']['type']
if t in ('nonstandard', 'pubkey', 'nulldata'):
if t in ('nonstandard', 'pubkey'):
return True
return False
@ -151,6 +151,7 @@ class unit_tests:
return await do_mmgen_ref(
('btc', 'btc_tn'),
(
'test/ref/tx/B498CE[5.55788,38].rawtx',
'test/ref/0B8D5A[15.31789,14,tl=1320969600].rawtx',
'test/ref/0C7115[15.86255,14,tl=1320969600].testnet.rawtx',
'test/ref/542169[5.68152,34].sigtx',

View file

@ -56,6 +56,7 @@ class unit_tests:
'Bitcoin',
(
'tx/7A8157[6.65227,34].rawtx',
'tx/B498CE[5.55788,38].rawtx',
'tx/BB3FD2[7.57134314,123].sigtx',
'tx/0A869F[1.23456,32].regtest.asubtx',
),
@ -112,12 +113,16 @@ class unit_tests:
return True
def op_return_data(self, name, ut, desc='OpReturnData class'):
max_len = cfg._proto.max_op_return_data_len
from mmgen.proto.btc.tx.op_return_data import OpReturnData
vecs = [
'data:=:ETH.ETH:0x86d526d6624AbC0178cF7296cD538Ecc080A95F1:0/1/0',
'hexdata:3d3a4554482e4554483a30783836643532366436363234416243303137'
'38634637323936634435333845636330383041393546313a302f312f30',
'hexdata:00010203040506',
'hexdata:' + 'ee' * max_len,
'data:' + 'z' * max_len,
'data:a',
'data:a\n',
'data:a\tb',
'data:' + gr_uc[:24],
@ -135,17 +140,19 @@ class unit_tests:
vmsg(repr(d))
vmsg(d.hl())
vmsg(d.hl(add_label=True))
vmsg(f'length: {len(str(d))}')
bad_data = [
'data:',
'hexdata:',
'data:' + ('x' * 81),
'data:' + 'x' * (max_len + 1),
'hexdata:' + ('deadbeef' * 20) + 'ee',
'hex:0abc',
'da:xyz',
'hexdata:xyz',
'hexdata:abcde',
b'data:abc',
'hexdata:' + 'dd' * (max_len + 1),
]
def bad(n):
@ -164,6 +171,7 @@ class unit_tests:
('bad7', 'AssertionError', 'not in hex', bad(6)),
('bad8', 'AssertionError', 'even', bad(7)),
('bad9', 'AssertionError', 'a string', bad(8)),
('bad10', 'AssertionError', 'not in range', bad(9)),
), pfx='')
return True

View file

@ -0,0 +1 @@
{"MMGenTransaction":{"coin_id":"BTC","chain":"mainnet","txid":"B498CE","send_amt":"5.55788","timestamp":"20250929_134955","blockcount":0,"serialized":"0200000001f887cf2ebaf99b7c2bccc8e6b1b2c57ff1481ce52a0c9f4baa1b517ee07322050600000000fdffffff040000000000000000536a4c50beadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafe0006f9000000000017a914727a067c447400c9b1487057a50cf0652299bd9b87e0a027200000000016001402d345bc687a1d02b651d11c4a64c3c91f51ba2262c64ded00000000160014cd11bb484e10f1da9e453c302d79b6cf219a81a000000000","inputs":[{"vout":6,"txid":"052273e07e511baa4b9f0c2ae51c48f17fc5b2b1e6c8cc2b7c9bf9ba2ecf87f8","scriptPubKey":"0014761f0cd436e84ca10c38d63a0ff318a49aa72f58","amt":"45.3709525","comment":"Ian\u2019s inheritance","addr":"bc1qwc0se4pkapx2zrpc6caqlucc5jd2wt6cj9fwnv","confs":354535,"mmid":"225E3732:B:5","sequence":4294967293}],"outputs":[{"amt":"0","data":"hexdata:beadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafebeadcafe"},{"addr":"3C8K8t4kfED7eXQKVJikhUA4UHgoo24C15","amt":"0.1632"},{"addr":"bc1qqtf5t0rg0gws9dj36ywy5exrey04rw3zfmxjcy","amt":"5.39468","mmid":"225E3732:B:12"},{"addr":"bc1qe5gmkjzwzrca48j98scz67dkeuse4qdqdzh5a4","amt":"39.8129725","is_chg":true,"mmid":"225E3732:B:99"}]},"chksum":"650a8f"}