Browse Source

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

The MMGen Project 2 months ago
parent
commit
4b55f1158e

+ 33 - 19
mmgen/proto/btc/tx/base.py

@@ -41,26 +41,40 @@ def decodeScriptPubKey(proto, s):
 	#   types: nonstandard, pubkey, pubkeyhash, scripthash, multisig, nulldata, witness_v0_keyhash
 	#   types: nonstandard, pubkey, pubkeyhash, scripthash, multisig, nulldata, witness_v0_keyhash
 	ret = namedtuple('decoded_scriptPubKey', ['type', 'addr_fmt', 'addr', 'data'])
 	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))
 					proto.max_op_return_data_len))
-
-	else:
-		raise NotImplementedError(f'Unrecognized scriptPubKey ({s})')
+		case _:
+			raise NotImplementedError(f'Unrecognized scriptPubKey ({s})')
 
 
 def DeserializeTX(proto, txhex):
 def DeserializeTX(proto, txhex):
 	"""
 	"""

+ 9 - 2
test/cmdtest_d/main.py

@@ -415,7 +415,9 @@ class CmdTestMain(CmdTestBase, CmdTestShared):
 		return self.addrgen(wf=None, dfl_wallet=True)
 		return self.addrgen(wf=None, dfl_wallet=True)
 
 
 	def txcreate_dfl_wallet(self, addrfile):
 	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):
 	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)
 		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,
 			do_label                   = False,
 			ss_args                    = [],
 			ss_args                    = [],
 			add_opts                   = [],
 			add_opts                   = [],
+			add_output_args            = [],
 			view                       = 'n',
 			view                       = 'n',
 			addrs_per_wallet           = addrs_per_wallet,
 			addrs_per_wallet           = addrs_per_wallet,
 			non_mmgen_input_compressed = True,
 			non_mmgen_input_compressed = True,
@@ -725,6 +728,7 @@ class CmdTestMain(CmdTestBase, CmdTestShared):
 			+ add_opts
 			+ add_opts
 			+ (make_input_opts() if cmdline_inputs else [])
 			+ (make_input_opts() if cmdline_inputs else [])
 			+ self._make_txcreate_outputs(tx_data)
 			+ self._make_txcreate_outputs(tx_data)
+			+ add_output_args
 			+ [tx_data[num]['addrfile'] for num in tx_data]
 			+ [tx_data[num]['addrfile'] for num in tx_data]
 			+ ss_args)
 			+ ss_args)
 
 
@@ -765,7 +769,10 @@ class CmdTestMain(CmdTestBase, CmdTestShared):
 		return t
 		return t
 
 
 	def txcreate(self, addrfile):
 	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=[]):
 	def txbump(self, txfile, prepend_args=[], seed_args=[]):
 		if not self.proto.cap('rbf'):
 		if not self.proto.cap('rbf'):

+ 2 - 1
test/daemontest_d/tx.py

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

+ 9 - 1
test/modtest_d/tx.py

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

+ 1 - 0
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"}