diff --git a/mmgen/proto/btc/tx/base.py b/mmgen/proto/btc/tx/base.py index 9a7431ad..f08aec7d 100755 --- a/mmgen/proto/btc/tx/base.py +++ b/mmgen/proto/btc/tx/base.py @@ -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): """ diff --git a/test/cmdtest_d/main.py b/test/cmdtest_d/main.py index c8165cb8..9fed9db2 100755 --- a/test/cmdtest_d/main.py +++ b/test/cmdtest_d/main.py @@ -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'): diff --git a/test/daemontest_d/tx.py b/test/daemontest_d/tx.py index 597c8200..7604b87c 100755 --- a/test/daemontest_d/tx.py +++ b/test/daemontest_d/tx.py @@ -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', diff --git a/test/modtest_d/tx.py b/test/modtest_d/tx.py index 5734695a..c93d4fc2 100755 --- a/test/modtest_d/tx.py +++ b/test/modtest_d/tx.py @@ -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 diff --git a/test/ref/tx/B498CE[5.55788,38].rawtx b/test/ref/tx/B498CE[5.55788,38].rawtx new file mode 100644 index 00000000..89e5c730 --- /dev/null +++ b/test/ref/tx/B498CE[5.55788,38].rawtx @@ -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"} \ No newline at end of file