base.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
  4. # Copyright (C)2013-2023 The MMGen Project <mmgen@tuta.io>
  5. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen
  9. # https://gitlab.com/mmgen/mmgen
  10. """
  11. proto.btc.tx.base: Bitcoin base transaction class
  12. """
  13. from collections import namedtuple
  14. import mmgen.tx.base as TxBase
  15. from ....obj import MMGenList,HexStr
  16. from ....util import msg,make_chksum_6,die,pp_fmt
  17. def addr2scriptPubKey(proto,addr):
  18. def decode_addr(proto,addr):
  19. ap = proto.decode_addr(addr)
  20. assert ap, f'coin address {addr!r} could not be parsed'
  21. return ap.bytes.hex()
  22. return {
  23. 'p2pkh': '76a914' + decode_addr(proto,addr) + '88ac',
  24. 'p2sh': 'a914' + decode_addr(proto,addr) + '87',
  25. 'bech32': proto.witness_vernum_hex + '14' + decode_addr(proto,addr)
  26. }[addr.addr_fmt]
  27. def scriptPubKey2addr(proto,s):
  28. if len(s) == 50 and s[:6] == '76a914' and s[-4:] == '88ac':
  29. return proto.pubhash2addr(bytes.fromhex(s[6:-4]),p2sh=False),'p2pkh'
  30. elif len(s) == 46 and s[:4] == 'a914' and s[-2:] == '87':
  31. return proto.pubhash2addr(bytes.fromhex(s[4:-2]),p2sh=True),'p2sh'
  32. elif len(s) == 44 and s[:4] == proto.witness_vernum_hex + '14':
  33. return proto.pubhash2bech32addr(bytes.fromhex(s[4:])),'bech32'
  34. else:
  35. raise NotImplementedError(f'Unknown scriptPubKey ({s})')
  36. def DeserializeTX(proto,txhex):
  37. """
  38. Parse a serialized Bitcoin transaction
  39. For checking purposes, additionally reconstructs the serialized TX without signature
  40. """
  41. def bytes2int(bytes_le):
  42. if bytes_le[-1] & 0x80: # sign bit is set
  43. die(3,"{}: Negative values not permitted in transaction!".format(bytes_le[::-1].hex()))
  44. return int(bytes_le[::-1].hex(),16)
  45. def bytes2coin_amt(bytes_le):
  46. return proto.coin_amt(bytes2int(bytes_le) * proto.coin_amt.satoshi)
  47. def bshift(n,skip=False,sub_null=False):
  48. nonlocal idx, raw_tx
  49. ret = tx[idx:idx+n]
  50. idx += n
  51. if sub_null:
  52. raw_tx += b'\x00'
  53. elif not skip:
  54. raw_tx += ret
  55. return ret
  56. # https://bitcoin.org/en/developer-reference#compactsize-unsigned-integers
  57. # For example, the number 515 is encoded as 0xfd0302.
  58. def readVInt(skip=False):
  59. nonlocal idx, raw_tx
  60. s = tx[idx]
  61. idx += 1
  62. if not skip:
  63. raw_tx.append(s)
  64. vbytes_len = 1 if s < 0xfd else 2 if s == 0xfd else 4 if s == 0xfe else 8
  65. if vbytes_len == 1:
  66. return s
  67. else:
  68. vbytes = tx[idx:idx+vbytes_len]
  69. idx += vbytes_len
  70. if not skip:
  71. raw_tx += vbytes
  72. return int(vbytes[::-1].hex(),16)
  73. def make_txid(tx_bytes):
  74. from hashlib import sha256
  75. return sha256(sha256(tx_bytes).digest()).digest()[::-1].hex()
  76. tx = bytes.fromhex(txhex)
  77. raw_tx = bytearray()
  78. idx = 0
  79. d = { 'version': bytes2int(bshift(4)) }
  80. has_witness = tx[idx] == 0
  81. if has_witness:
  82. u = bshift(2,skip=True).hex()
  83. if u != '0001':
  84. die( 'IllegalWitnessFlagValue', f'{u!r}: Illegal value for flag in transaction!' )
  85. d['num_txins'] = readVInt()
  86. d['txins'] = MMGenList([{
  87. 'txid': bshift(32)[::-1].hex(),
  88. 'vout': bytes2int(bshift(4)),
  89. 'scriptSig': bshift(readVInt(skip=True),sub_null=True).hex(),
  90. 'nSeq': bshift(4)[::-1].hex()
  91. } for i in range(d['num_txins'])])
  92. d['num_txouts'] = readVInt()
  93. d['txouts'] = MMGenList([{
  94. 'amount': bytes2coin_amt(bshift(8)),
  95. 'scriptPubKey': bshift(readVInt()).hex()
  96. } for i in range(d['num_txouts'])])
  97. for o in d['txouts']:
  98. o['address'] = scriptPubKey2addr(proto,o['scriptPubKey'])[0]
  99. if has_witness:
  100. # https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
  101. # A non-witness program (defined hereinafter) txin MUST be associated with an empty
  102. # witness field, represented by a 0x00.
  103. d['txid'] = make_txid(tx[:4] + tx[6:idx] + tx[-4:])
  104. d['witness_size'] = len(tx) - idx + 2 - 4 # add len(marker+flag), subtract len(locktime)
  105. for txin in d['txins']:
  106. if tx[idx] == 0:
  107. bshift(1,skip=True)
  108. continue
  109. txin['witness'] = [
  110. bshift(readVInt(skip=True),skip=True).hex() for item in range(readVInt(skip=True)) ]
  111. else:
  112. d['txid'] = make_txid(tx)
  113. d['witness_size'] = 0
  114. if len(tx) - idx != 4:
  115. die( 'TxHexParseError', 'TX hex has invalid length: {} extra bytes'.format(len(tx)-idx-4) )
  116. d['locktime'] = bytes2int(bshift(4))
  117. d['unsigned_hex'] = raw_tx.hex()
  118. return namedtuple('deserialized_tx',list(d.keys()))(**d)
  119. class Base(TxBase.Base):
  120. rel_fee_desc = 'satoshis per byte'
  121. rel_fee_disp = 'sat/byte'
  122. _deserialized = None
  123. class InputList(TxBase.Base.InputList):
  124. # Lexicographical Indexing of Transaction Inputs and Outputs
  125. # https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki
  126. def sort_bip69(self):
  127. def sort_func(a):
  128. return (
  129. bytes.fromhex(a.txid)
  130. + int.to_bytes(a.vout,4,'big') )
  131. self.sort(key=sort_func)
  132. class OutputList(TxBase.Base.OutputList):
  133. def sort_bip69(self):
  134. def sort_func(a):
  135. return (
  136. int.to_bytes(a.amt.to_unit('satoshi'),8,'big')
  137. + bytes.fromhex(addr2scriptPubKey(self.parent.proto,a.addr)) )
  138. self.sort(key=sort_func)
  139. def has_segwit_inputs(self):
  140. return any(i.mmtype in ('S','B') for i in self.inputs)
  141. def has_segwit_outputs(self):
  142. return any(o.mmtype in ('S','B') for o in self.outputs)
  143. # https://bitcoin.stackexchange.com/questions/1195/how-to-calculate-transaction-size-before-sending
  144. # 180: uncompressed, 148: compressed
  145. def estimate_size_old(self):
  146. if not self.inputs or not self.outputs:
  147. return None
  148. return len(self.inputs)*180 + len(self.outputs)*34 + 10
  149. # https://bitcoincore.org/en/segwit_wallet_dev/
  150. # vsize: 3 times of the size with original serialization, plus the size with new
  151. # serialization, divide the result by 4 and round up to the next integer.
  152. # TODO: results differ slightly from actual transaction size
  153. def estimate_size(self):
  154. if not self.inputs or not self.outputs:
  155. return None
  156. sig_size = 72 # sig in DER format
  157. pubkey_size_uncompressed = 65
  158. pubkey_size_compressed = 33
  159. def get_inputs_size():
  160. # txid vout [scriptSig size (vInt)] scriptSig (<sig> <pubkey>) nSeq
  161. isize_common = 32 + 4 + 1 + 4 # txid vout [scriptSig size] nSeq = 41
  162. input_size = {
  163. 'L': isize_common + sig_size + pubkey_size_uncompressed, # = 180
  164. 'C': isize_common + sig_size + pubkey_size_compressed, # = 148
  165. 'S': isize_common + 23, # = 64
  166. 'B': isize_common + 0 # = 41
  167. }
  168. ret = sum(input_size[i.mmtype] for i in self.inputs if i.mmtype)
  169. # We have no way of knowing whether a non-MMGen P2PKH addr is compressed or uncompressed
  170. # until we see the key, so assume compressed for fee-estimation purposes. If fee estimate
  171. # is off by more than 5%, sign() aborts and user is instructed to use --vsize-adj option.
  172. return ret + sum(input_size['C'] for i in self.inputs if not i.mmtype)
  173. def get_outputs_size():
  174. # output bytes = amt: 8, byte_count: 1+, pk_script
  175. # pk_script bytes: p2pkh: 25, p2sh: 23, bech32: 22
  176. return sum({'p2pkh':34,'p2sh':32,'bech32':31}[o.addr.addr_fmt] for o in self.outputs)
  177. # https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
  178. # The witness is a serialization of all witness data of the transaction. Each txin is
  179. # associated with a witness field. A witness field starts with a var_int to indicate the
  180. # number of stack items for the txin. It is followed by stack items, with each item starts
  181. # with a var_int to indicate the length. Witness data is NOT script.
  182. # A non-witness program txin MUST be associated with an empty witness field, represented
  183. # by a 0x00. If all txins are not witness program, a transaction's wtxid is equal to its txid.
  184. def get_witness_size():
  185. if not self.has_segwit_inputs():
  186. return 0
  187. wf_size = 1 + 1 + sig_size + 1 + pubkey_size_compressed # vInt vInt sig vInt pubkey = 108
  188. return sum((1,wf_size)[i.mmtype in ('S','B')] for i in self.inputs)
  189. isize = get_inputs_size()
  190. osize = get_outputs_size()
  191. wsize = get_witness_size()
  192. # TODO: compute real varInt sizes instead of assuming 1 byte
  193. # Serialization:
  194. # old: [nVersion] [vInt][txins][vInt][txouts] [nLockTime]
  195. old_size = 4 + 1 + isize + 1 + osize + 4
  196. # marker = 0x00, flag = 0x01
  197. # new: [nVersion][marker][flag][vInt][txins][vInt][txouts][witness][nLockTime]
  198. new_size = 4 + 1 + 1 + 1 + isize + 1 + osize + wsize + 4 if wsize else old_size
  199. ret = (old_size * 3 + new_size) // 4
  200. self.cfg._util.dmsg(
  201. '\nData from estimate_size():\n' +
  202. f' inputs size: {isize}, outputs size: {osize}, witness size: {wsize}\n' +
  203. f' size: {new_size}, vsize: {ret}, old_size: {old_size}' )
  204. return int(ret * (self.cfg.vsize_adj or 1))
  205. # convert absolute CoinAmt fee to sat/byte using estimated size
  206. def fee_abs2rel(self,abs_fee,to_unit='satoshi'):
  207. return int(
  208. abs_fee /
  209. getattr( self.proto.coin_amt, to_unit ) /
  210. self.estimate_size() )
  211. @property
  212. def deserialized(self):
  213. if not self._deserialized:
  214. self._deserialized = DeserializeTX(self.proto,self.serialized)
  215. return self._deserialized
  216. def update_serialized(self,data):
  217. self.serialized = HexStr(data)
  218. self._deserialized = None
  219. self.check_serialized_integrity()
  220. def check_serialized_integrity(self):
  221. """
  222. Check that a malicious, compromised or malfunctioning coin daemon hasn't produced bad
  223. serialized tx data.
  224. Does not check witness data.
  225. Perform this check every time a serialized tx is received from the coin daemon or read
  226. from a transaction file.
  227. """
  228. def do_error(errmsg):
  229. die( 'TxHexMismatch', errmsg+'\n'+hdr )
  230. def check_equal(desc,hexio,mmio):
  231. if mmio != hexio:
  232. msg('\nMMGen {d}:\n{m}\nSerialized {d}:\n{h}'.format(
  233. d = desc,
  234. m = pp_fmt(mmio),
  235. h = pp_fmt(hexio) ))
  236. do_error(
  237. f'{desc.capitalize()} in serialized transaction data from coin daemon ' +
  238. 'do not match those in MMGen transaction!' )
  239. hdr = 'A malicious or malfunctioning coin daemon or other program may have altered your data!'
  240. dtx = self.deserialized
  241. if dtx.locktime != int(self.locktime or 0):
  242. do_error(
  243. f'Transaction hex nLockTime ({dtx.locktime}) ' +
  244. f'does not match MMGen transaction nLockTime ({self.locktime})' )
  245. check_equal(
  246. 'sequence numbers',
  247. [i['nSeq'] for i in dtx.txins],
  248. ['{:08x}'.format(i.sequence or self.proto.max_int) for i in self.inputs] )
  249. check_equal(
  250. 'inputs',
  251. sorted((i['txid'],i['vout']) for i in dtx.txins),
  252. sorted((i.txid,i.vout) for i in self.inputs) )
  253. check_equal(
  254. 'outputs',
  255. sorted((o['address'],self.proto.coin_amt(o['amount'])) for o in dtx.txouts),
  256. sorted((o.addr,o.amt) for o in self.outputs) )
  257. if str(self.txid) != make_chksum_6(bytes.fromhex(dtx.unsigned_hex)).upper():
  258. do_error(
  259. f'MMGen TxID ({self.txid}) does not match serialized transaction data!')