Skip to content

Commit f16d532

Browse files
committed
major checkpoint; we can now manually create and digitally sign totally valid p2pkh transactions! much wow. a lot of things came together right here.
1 parent 1cc5f64 commit f16d532

File tree

2 files changed

+77
-11
lines changed

2 files changed

+77
-11
lines changed

cryptos/transaction.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class TxFetcher:
5555
""" lazily fetches transactions using an api on demand """
5656

5757
@staticmethod
58-
def fetch(tx_id: str):
58+
def fetch(tx_id: str, net: str):
5959
assert isinstance(tx_id, str)
6060
assert all(c in string.hexdigits for c in tx_id)
6161
tx_id = tx_id.lower() # normalize just in case we get caps
@@ -71,8 +71,15 @@ def fetch(tx_id: str):
7171
else:
7272
# fetch bytes from api
7373
# print("fetching transaction %s from API" % (tx_id, ))
74-
url = 'https://blockstream.info/api/tx/%s/hex' % (tx_id, )
74+
assert net is not None, "can't fetch a transaction without knowing which net to look at, e.g. main|test"
75+
if net == 'main':
76+
url = 'https://blockstream.info/api/tx/%s/hex' % (tx_id, )
77+
elif net == 'test':
78+
url = 'https://blockstream.info/testnet/api/tx/%s/hex' % (tx_id, )
79+
else:
80+
raise ValueError("%s is not a valid net type, should be main|test" % (net, ))
7581
response = requests.get(url)
82+
assert response.status_code == 200, "transaction id %s was not found on blockstream" % (tx_id, )
7683
raw = bytes.fromhex(response.text.strip())
7784
# cache on disk
7885
if not os.path.isdir(txdb_dir):
@@ -81,6 +88,7 @@ def fetch(tx_id: str):
8188
f.write(raw)
8289

8390
tx = Tx.decode(BytesIO(raw))
91+
assert tx.id() == tx_id # ensure that the calculated id matches the request id
8492
return tx
8593

8694

@@ -89,8 +97,8 @@ class Tx:
8997
version: int
9098
tx_ins: List[TxIn]
9199
tx_outs: List[TxOut]
92-
locktime: int
93-
segwit: bool
100+
locktime: int = 0
101+
segwit: bool = False
94102

95103
@classmethod
96104
def decode(cls, s):
@@ -201,9 +209,10 @@ def validate(self):
201209
class TxIn:
202210
prev_tx: bytes # prev transaction ID: hash256 of prev tx contents
203211
prev_index: int # UTXO output index in the transaction
204-
script_sig: Script # unlocking script
205-
sequence: int # originally intended for "high frequency trades", with locktime
212+
script_sig: Script = None # unlocking script
213+
sequence: int = 0xffffffff # originally intended for "high frequency trades", with locktime
206214
witness: List[bytes] = None
215+
net: str = None # which net are we on? eg 'main'|'test'
207216

208217
@classmethod
209218
def decode(cls, s):
@@ -223,7 +232,7 @@ def encode(self, script_override=None):
223232
out += [self.script_sig.encode()]
224233
elif script_override is True:
225234
# True = override the script with the script_pubkey of the associated input
226-
tx = TxFetcher.fetch(self.prev_tx.hex())
235+
tx = TxFetcher.fetch(self.prev_tx.hex(), net=self.net)
227236
out += [tx.tx_outs[self.prev_index].script_pubkey.encode()]
228237
elif script_override is False:
229238
# False = override with an empty script
@@ -236,13 +245,13 @@ def encode(self, script_override=None):
236245

237246
def value(self):
238247
# look the amount up on the previous transaction
239-
tx = TxFetcher.fetch(self.prev_tx.hex())
248+
tx = TxFetcher.fetch(self.prev_tx.hex(), net=self.net)
240249
amount = tx.tx_outs[self.prev_index].amount
241250
return amount
242251

243252
def script_pubkey(self):
244253
# look the script_pubkey up on the previous transaction
245-
tx = TxFetcher.fetch(self.prev_tx.hex())
254+
tx = TxFetcher.fetch(self.prev_tx.hex(), net=self.net)
246255
script = tx.tx_outs[self.prev_index].script_pubkey
247256
return script
248257

tests/test_tx.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
Test Transaction
33
"""
44

5-
from cryptos.transaction import Tx
65
from io import BytesIO
76

7+
from cryptos.transaction import Tx, TxIn, TxOut, Script
8+
from cryptos.btc_address import address_to_pkb_hash
9+
from cryptos.keys import sk_to_pk, pk_to_sec
10+
from cryptos.ecdsa import sign
11+
812
def test_legacy_decode():
913

1014
# Example taken from Programming Bitcoin, Chapter 5
@@ -38,7 +42,7 @@ def test_legacy_decode():
3842
# validate the transaction as Bitcoin law-abiding and cryptographically authentic
3943
assert tx.validate()
4044

41-
# fudge the r in the (r,s) digital signature tuple, this should break validation because CEHCKSIG will fail
45+
# fudge the r in the (r,s) digital signature tuple, this should break validation because CHECKSIG will fail
4246
sigb = tx.tx_ins[0].script_sig.cmds[0]
4347
sigb2 = sigb[:6] + bytes([(sigb[6] + 1) % 255]) + sigb[7:]
4448
tx.tx_ins[0].script_sig.cmds[0] = sigb2
@@ -79,3 +83,56 @@ def test_segwit_decode():
7983
# check correct decoding/encoding
8084
raw2 = tx.encode()
8185
assert raw == raw2
86+
87+
def test_create_tx():
88+
# this example follows Programming Bitcoin Chapter 7
89+
90+
# define the inputs of our aspiring transaction
91+
prev_tx = bytes.fromhex('0d6fe5213c0b3291f208cba8bfb59b7476dffacc4e5cb66f6eb20a080843a299')
92+
prev_index = 13
93+
tx_in = TxIn(prev_tx, prev_index, net='test')
94+
95+
# change output that goes back to us
96+
amount = int(0.33 * 1e8) # 0.33 tBTC in units of satoshi
97+
pkb_hash = address_to_pkb_hash('mzx5YhAH9kNHtcN481u6WkjeHjYtVeKVh2')
98+
script = Script([118, 169, pkb_hash, 136, 172]) # OP_DUP, OP_HASH160, <hash>, OP_EQUALVERIFY, OP_CHECKSIG
99+
tx_out_change = TxOut(amount=amount, script_pubkey=script)
100+
101+
# target output that goes to a lucky recepient
102+
amount = int(0.1 * 1e8) # 0.1 tBTC in units of satoshi
103+
pkb_hash = address_to_pkb_hash('mnrVtF8DWjMu839VW3rBfgYaAfKk8983Xf')
104+
script = Script([118, 169, pkb_hash, 136, 172]) # OP_DUP, OP_HASH160, <hash>, OP_EQUALVERIFY, OP_CHECKSIG
105+
tx_out_target = TxOut(amount=amount, script_pubkey=script)
106+
107+
# create the desired transaction object
108+
tx = Tx(1, [tx_in], [tx_out_change, tx_out_target])
109+
110+
# validate the intended fee of 0.01 tBTC
111+
assert tx.fee() == int(0.01 * 1e8)
112+
113+
# produce the unlocking script for this p2pkh tx: [<signature>, <pubkey>]
114+
115+
# first produce the <pubkey> that will satisfy OP_EQUALVERIFY on the locking script
116+
sk = 8675309 # the secret key that produced the public key that produced the hash that is on that input tx's locking script
117+
pk = sk_to_pk(sk)
118+
sec = pk_to_sec(pk, compressed=True) # sec encoded public key as bytes
119+
# okay but anyone with the knowledge of the public key could have done this part if this public
120+
# key was previously used (and hence revealed) somewhere on the blockchain
121+
122+
# now produce the digital signature that will satisfy the OP_CHECKSIG on the locking script
123+
enc = tx.encode(sig_index=0)
124+
sig = sign(sk, enc) # only and uniquely the person with the secret key can do this
125+
der = sig.encode()
126+
der_and_type = der + b'\x01' # 1 = SIGHASH_ALL, indicating this der signature encoded "ALL" of the tx
127+
128+
# set the unlocking script into the transaction
129+
tx_in.script_sig = Script([der_and_type, sec])
130+
131+
# final check: ensure that our manually constructed transaction is all valid and ready to send out to the wild
132+
assert tx.validate()
133+
134+
# peace of mind: fudge the signature and try again
135+
der = der[:6] + bytes([(der[6] + 1) % 255]) + der[7:]
136+
der_and_type = der + b'\x01'
137+
tx_in.script_sig = Script([der_and_type, sec])
138+
assert not tx.validate()

0 commit comments

Comments
 (0)