""" WO-12 exam, written step by step by the owner and Claude. STATUS: FINISHED, 8 October 2026. All 6 exam tests pass (about 3 s): A ACCEPT, A2 ACCEPT, S REJECT, B1 REJECT, B2 REJECT, B3 REJECT. Steps 1-6 were written together with the owner, one function at a time; step 7 was drafted by Claude at the owner's request and run the same day. Run from anywhere: python3 -I e46/tests/exam_wo12.py BEFORE YOU RUN IT: set aside at least 1 HOUR to read it first. The comments are the point: they explain how an E46 transaction is built, byte by byte, in plain English. Read top to bottom; each step builds on the one before. TL;DR 1 compact_size write a count in as few bytes as possible (signposts FD/FE/FF) 2 varbytes a length, then the data 3 tagged_hash a labelled SHAKE256 fingerprint (no SHA-2, owner's rule) 4 serialize_* inputs point at old slots (coins); outputs make new slots; lock_time can post-date a payment; witnesses go last, so the txid ignores signatures and the wtxid includes them 5 sighash the exact message a key signs: the whole transaction, which input, and every spent coin's amount and lock 6 keys and signing real SLH-DSA keys (C++ tool), the lock in pure Python 7 THE EXAM A honest payment, real signature -> must be ACCEPTED B1 one output of T + 1 base units -> must be REJECTED B2 two outputs that together pass T -> must be REJECTED Because A passes with the same keys and format, B1/B2 can only be rejected by the amount rule (decision #20d). Started 8 October 2026, written one function at a time: each step was explained, questioned and rewritten in plain English before it was saved. Time taken: (owner to fill in) Tokens used: not measured inside the session (Claude can't count its own usage reliably; see the app's usage view if you want the number). From the owner: I'm grateful for this exam. Building it slowly, question by question, clarified how E46 transactions really work, for me and for everyone who reads this file. Goal: build E46 transactions with REAL signatures and check the C++ transaction tool against this independent Python, so that when an over-the-limit amount is rejected we know the amount rule caught it, not a bad signature. """ # --------------------------------------------------------------------------- # IMPORTS: standard library only, so anyone can replicate this exam # struct turn numbers into bytes # hashlib SHAKE256, built into Python (no install needed) # subprocess runs ONE C++ tool, keys_cli, for SLH-DSA signing only # Plus the repo's own pure-Python Refracting Light reference # (refracting_light_v3.py), so the address lock needs no C++ tool. # # Why we don't swap in a different hash for the lock: the lock is fixed # by consensus (RL v3 128 + SHAKE256 128). A different hash would make # the C++ checker reject even the honest payment. # Why signing still uses C++: Python's standard library has no SLH-DSA, # and writing our own signature code is ruled out (decision #19). # --------------------------------------------------------------------------- import struct import hashlib import subprocess import sys import os # Work from the repository root, wherever this file is run from: # this file lives in e46/tests/, so the root is two folders up. os.chdir(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) sys.path.insert(0, ".") # the repo root, for the RL reference from refracting_light_v3 import digest as refracting_light # --------------------------------------------------------------------------- # Step 1: compact_size # --------------------------------------------------------------------------- def compact_size(n): """ Write a count (like "3 inputs" or "7,856 bytes of signature") in as few bytes as possible. E46 uses this before every list and every piece of data, so the reader knows how much is coming. HOW IT WORKS 0 to 252 -> the count fits in one byte: just write it. 253, 254, 255 are RESERVED signposts, never ordinary counts (like reserved addresses in a memory map): 253 = "the next 2 bytes are the count" (up to 65,535) 254 = "the next 4 bytes are the count" (up to about 4.3 billion) 255 = "the next 8 bytes are the count" (anything bigger) Nothing overflows and nothing is overwritten: a count too big for one byte simply moves to a bigger box, with a signpost first. WHY HEX A byte holds 0 to 255. In hex (base 16) every byte is exactly two characters, 00 to FF, so byte boundaries are easy to see and the box limits become round-looking numbers (FF, FFFF, FFFFFFFF). The spec, Bitcoin's docs and every hex dump of a transaction use hex, so the code matches what you'll see in the real bytes. The 0x in front just means "this number is hex". Translations: hex plain number meaning here 0xFC 252 biggest count that fits in one byte 0xFD 253 signpost: 2-byte count follows 0xFE 254 signpost: 4-byte count follows 0xFF 255 signpost: 8-byte count follows 0xFFFF 65,535 biggest 2-byte count 0xFFFFFFFF 4,294,967,295 biggest 4-byte count 0x1EB0 7,856 every E46 signature length EXAMPLES (hex bytes, with plain numbers in brackets) compact_size(3) -> 03 1 byte [3] compact_size(252) -> FC 1 byte [252] compact_size(253) -> FD FD 00 3 bytes [signpost 253] + 253 in 2 bytes compact_size(7856) -> FD B0 1E 3 bytes [signpost 253] + 7,856 in 2 bytes 7,856 = 0x1EB0: big part 1E (30), small part B0 (176), written smallest part first -> B0 1E THE TAIL The "tail" is everything written AFTER the count: the inputs, outputs, keys or signature that the count describes. A bigger count pushes the tail later. Nothing is overwritten; it just starts further along: count 3: 03 [tail starts here...] position 0, tail at 1 count 7856: FD B0 1E [tail starts here...] positions 0-2, tail at 3 The reader always reads the first byte first, so it knows how long the count is and exactly where the tail begins. ONE SPELLING ONLY 3 could also be spelled the long way: FD 03 00. That would put the tail in a different place and give the same transaction a second txid. The box limits (252, 65,535, ...) don't prevent that on their own; a separate rule does: the SHORTEST spelling is the only valid one. That rule stops MALLEABILITY, which is the opposite of a collision: collision = two DIFFERENT transactions -> the SAME txid malleability = ONE transaction -> two DIFFERENT txids (trackers lose the payment; anything built on the txid breaks) A SECOND ADDRESS Paying two people means two outputs. The output count becomes 2 (still one byte), and the tail grows by one more output: [2] [ output 1 ] [ output 2 ] [lock_time] ... count value type len address value type len address 8 B 1 B [32] 32 B 8 B 1 B [32] 32 B ------- 42 bytes ------- ------- 42 bytes ------- Everything after output 2 (lock_time, then the signatures) starts 42 bytes later. The txid changes, because it's a different transaction. The signature covers every output, so nobody can swap an address after you sign. A second address costs 42 bytes; the signature costs about 7,900, so paying more people is cheap and signing is what's heavy. """ # " 02 68 69 02 = length 2, then "h" (0x68) and "i" (0x69) varbytes(b"") -> 00 length 0, nothing follows varbytes(32-byte address) -> 20 + the 32 address bytes = 33 bytes total varbytes(7,856-byte sig) -> FD B0 1E + the signature bytes = 7,859 bytes total WHY THE LENGTH GOES FIRST Without it, the reader can't tell where a signature ends and the next thing begins. With it, the reader reads the length, takes exactly that many bytes, and the tail starts right after. """ return compact_size(len(data)) + data # --------------------------------------------------------------------------- # Step 3: tagged_hash # --------------------------------------------------------------------------- def tagged_hash(tag, data): """ Make a 32-byte fingerprint of `data`, labelled with `tag`. tagged_hash(tag, data) = SHAKE256(tag + data), first 32 bytes A fingerprint (hash) turns any amount of data into 32 bytes. Change one bit of the data and the fingerprint changes completely. Working backwards from a fingerprint to the data is believed to be infeasible: no method better than guessing is known, and guessing takes about 2^256 tries (about 2^128 even with a quantum computer running Grover's algorithm). "Believed", not proven: that's the honest status of every hash function, SHAKE256 included. THE LABEL (tag) Every E46 fingerprint starts with a label that says what it's for: b"E46-txid\\0" the transaction ID (no signatures) b"E46-wtxid\\0" the full transaction ID (with signatures) b"E46-sighash\\0" the message a key signs The \\0 at the end is one zero byte (0x00): a full stop that marks where the label ends and the data begins. WHY LABEL IT Without labels, the same bytes would give the same fingerprint whatever job they were doing. A txid could then be presented as a sighash, or the other way round. With labels, the same data gives completely different fingerprints for different jobs. TXID AND WTXID ARE NOT A HUNT Nobody tries to make a txid match a wtxid, and nothing here is halved or folded. They are two separate fingerprints of two different inputs with two different labels: txid = fingerprint of the transaction WITHOUT signatures wtxid = fingerprint of the transaction WITH signatures They are never expected to match. The txid stays fixed even if a signature is changed; the wtxid changes. That's the whole point. (The places E46 does hunt are mining and Rads, using Refracting Light, not these SHAKE256 fingerprints. Halves appear only in the address payload, which joins two separate 128-bit fingerprints, Refracting Light and SHAKE256, side by side.) THE NO-SHA-2 RULE (owner's rule, 7 Oct 2026) E46 uses no SHA-2 (SHA-256, SHA-256d) anywhere. SHAKE256 comes from the SHA-3 family, built on a different design (a "sponge"). Why: just in case SHA-2 is ever broken. Bitcoin runs on SHA-256; if a weakness is found there, E46 is unaffected, and anything E46 learns is independent evidence rather than a copy of Bitcoin's. A bonus: SHA-3's design is immune to the "length-extension" trick that plain SHA-256 is open to. EXAMPLES (first 8 of the 32 bytes, hex) tagged_hash(b"E46-txid\\0", b"hi") -> 21 3E 4F 4C 7D C4 20 5A ... tagged_hash(b"E46-wtxid\\0", b"hi") -> 47 8A A0 D8 8B 53 55 B3 ... same data, different label: a completely different fingerprint tagged_hash(b"E46-txid\\0", b"hj") -> BF F5 15 92 BD CA 4A D0 ... one letter changed (i -> j): also completely different """ return hashlib.shake_256(tag + data).digest(32) # --------------------------------------------------------------------------- # Step 4, piece 1: serialize_input # --------------------------------------------------------------------------- # # HOW COINS WORK IN E46 (notes from the owner's questions) # # A "coin" is an OUTPUT: a slot in an old transaction, holding an amount # and a lock (an address = a fingerprint of the owner's public key). # There are no account balances; your balance is all the unspent slots # you can open. # # OLD TRANSACTION (txid AB...AB) # slot 0: 5 E46 lock = Bob's address # slot 1: 10 E46 lock = YOUR address <---+ # | the input says: # YOUR NEW TRANSACTION | "txid AB...AB, slot 1" # INPUT: (AB...AB, slot 1) --------------+ <- points at the slot # OUTPUT: 7.00 E46 to Alice # OUTPUT: 2.99 E46 change back to you # lock_time # WITNESS (separate section at the end) # your public key (32 bytes) <- matches the lock # your signature (7,856 bytes) <- proves you approved THIS transaction # # POINTING: the input works like a cheque number. It doesn't copy the # amount or the lock; it names the slot. The pair (txid, slot) is a # unique, permanent name for one coin, like an ordinal names one # satoshi. Bitcoiners call it an "outpoint". # # WHO WRITES SLOTS # create slots (outputs) only the transaction's author, when written # point at any slot anyone; it's only a reference # actually spend a slot only whoever can open its lock # spend a slot twice nobody; the network rejects it # Once a transaction is in a block, its slots are fixed forever. # # CHANGE AND FEE # INPUT 10.00 E46 # OUTPUT 7.00 E46 -> Alice (new slot, Alice owns it) # OUTPUT 2.99 E46 -> you, change (new slot, you own it, ready for # your next payment) # FEE 0.01 E46 -> the miner (never written as a slot: it's # the gap, 10.00 - 9.99) # If a wallet forgot the change output, the whole 3.00 would go to the # miner. Every node stores every byte forever; the fee is how you pay # for the space, so fees are usually priced per byte. # # THE WITNESS IS ONLY IN THE WTXID # txid = version + inputs + outputs + lock_time (no witness) # wtxid = all of that + the witness # The block still seals every wtxid (the witness commitment in the # coinbase, decision #20a), so signatures are kept out of the payment's # name but still locked into the chain. (Owner's picture: the witness # is like the mirror at an event horizon, there, but outside the name.) # # SIGNING HAPPENS BEFORE SUBMITTING, ON YOUR DEVICE # 1. build the payment -> txid already knowable # 2. sign it -> witness added # 3. fingerprint all -> wtxid # 4. submit -> the network sees ONE complete, signed transaction # No second write; the network rejects anything unsigned. The txid only # changes if YOU change the payment (new amount, higher fee, another coin): # that's a new transaction. Nobody else can change it by tampering with # the signature. def serialize_input(prev_txid, prev_index, sequence): """ Write one input: a pointer to the coin being spent. input = prev_txid (32 bytes) + prev_index (4 bytes) + sequence (4 bytes) = 40 bytes, always WHAT EACH PART MEANS prev_txid the txid of the transaction that created the coin ("which envelope") prev_index which output of that transaction: 0 = first, 1 = second, ... ("which slot in it") sequence a spare dial for future features; usually FF FF FF FF (4,294,967,295), meaning "no special rules" WHY SEQUENCE EXISTS (kept on purpose, "just in case") Inherited from Bitcoin. Bitcoin uses it every day for replace-by-fee (bumping a stuck payment's fee) and for relative time locks (BIP68). E46 leaves relative locks for a later soft fork, so this slot stays free for them. 4 bytes per input is about 0.05% of a signature, so the room is nearly free. No length is needed in front (unlike varbytes): an input is always exactly 40 bytes, so the reader always knows where it ends. EXAMPLE (hex) Spending output 1 of transaction AB AB AB ... AB: prev_txid AB AB AB ... AB 32 bytes prev_index 01 00 00 00 [1], smallest part first sequence FF FF FF FF [4,294,967,295] total 40 bytes THE COINBASE INPUT (new coins, no real coin being spent) prev_txid 00 00 00 ... 00 32 zero bytes: "no previous transaction" prev_index FF FF FF FF [4,294,967,295]: "no slot" """ return prev_txid + struct.pack(" 20 + 32 bytes EXAMPLES (hex, plain numbers in brackets) 7.00 E46 to Alice = 700,000,000 base units (0x29B92700) value 00 27 B9 29 00 00 00 00 [700,000,000], smallest part first (big-to-small 29 B9 27 00, written small-first, then four 00 bytes to fill all 8) type 00 [one owner] payload 20 + Alice's 32-byte address total 8 + 1 + 33 = 42 bytes 2.99 E46 change to you = 299,000,000 base units (0x11D260C0) value C0 60 D2 11 00 00 00 00 [299,000,000] type 00 [one owner] payload 20 + your 32-byte address total 42 bytes A data note (like the genesis headline): value must be 0 value 00 00 00 00 00 00 00 00 [0] type 03 [data note] payload length + up to 80 bytes of text This function only WRITES the slot. The checking step later tests that the C++ rejects any value above T (46 million E46) and any total of outputs larger than the inputs (decision #20d). """ return struct.pack(" used for the txid full = base + the witness section -> used for the wtxid THE LAYOUT, IN ORDER version 4 bytes which rules this transaction follows (1 for now) input count compact_size step 1 inputs 40 bytes each piece 1 output count compact_size step 1 outputs ~42 bytes each piece 2 lock_time 8 bytes see LOCK_TIME below ---------------- end of base ------------------------------------------- witnesses one per input, in the same order as the inputs inputs a list of (prev_txid, prev_index, sequence) outputs a list of (value, type, payload) witnesses a list of ready-made witness bytes, one per input: a normal spend: varbytes(public key) + varbytes(signature) = 20 + 32 bytes + FD B0 1E + 7,856 bytes the coinbase: varbytes(extranonce [8 bytes] + up to 100 free bytes) EXAMPLE: you pay Alice 7.00 and get 2.99 change (1 input, 2 outputs) version 01 00 00 00 4 input count 01 1 input (AB...AB, slot 1, FF FF FF FF) 40 output count 02 1 output 1 7.00 E46 to Alice 42 output 2 2.99 E46 change to you 42 lock_time 00 00 00 00 00 00 00 00 8 base = 138 bytes witness 20 + key, FD B0 1E + signature 7,892 full = 8,030 bytes (1 input, 1 output: base 96 + witness 7,892 = 7,988 bytes) The base is under 2% of the full transaction: the signature is almost everything. That's the post-quantum price. WHY THE WITNESSES GO LAST So the base is one unbroken run of bytes. The txid fingerprints exactly that run; the witness section hangs off the end, outside it. LOCK_TIME (8 bytes, the last part of the base) "This transaction can't go into a block before this point." One number, read two ways depending on its size: value meaning hex (smallest part first) 0 no lock: valid right away 00 00 00 00 00 00 00 00 1 to 499,999,999 a BLOCK HEIGHT: not before e.g. 105,000 -> that block 28 9A 01 00 00 00 00 00 500,000,000 and above a UNIX TIME (seconds since e.g. 1,798,761,600 = 1 Jan 2027 -> 1 Jan 1970): not before then 80 EC 36 6B 00 00 00 00 Why the split at 500,000,000: as a block height, that's about 2,400 years of E46 blocks away; as a time, it's November 1985, long past. So the two meanings can never be confused. WHY "USUALLY 0" Most payments should go through now, so wallets write 0: no lock. (Some wallets write the current block height instead. It's still valid right away, but it stops a miner from rewriting recent blocks just to grab your fee: "anti fee-sniping", a habit of Bitcoin Core.) WHEN IT'S NOT 0 a post-dated payment "pay the landlord, but not before the 1st" inheritance "this goes to my family if I haven't moved the coins myself by 2030" the coinbase must equal the block height (decision #20), which makes every coinbase txid unique lock_time is inside the base, so it's part of the txid and the signature covers it: nobody can change the date after you sign. NOTE: YES, YOU CAN POST-DATE PAYMENTS (owner: "that's big") Sign today, hand the signed transaction to the payee, and the network won't accept it until the date or block you chose. Like a post-dated cheque, with one honest catch: until then the coins haven't moved, so the payer could still spend them elsewhere first, which would make the post-dated one invalid. It's a promise the chain enforces the timing of, not a guarantee the money will still be there. OPEN (decision #22): the spec says "as in Bitcoin" but doesn't yet say (1) whether the lock opens AT or AFTER the chosen block, or (2) whether lock_time is switched off when every input's sequence is FF FF FF FF, as it is in Bitcoin. To decide before block validation is built. """ base = ( struct.pack(" tagged_hash of all that -> 32 bytes. That's what your key signs. """ msg = base + struct.pack(" public key + private key + lock (address payload) # | | +--> goes in the OLD slot being spent # | +--> sign(sighash) --> 7,856-byte signature # +-----------------------------+ | # v v # witness = varbytes(public key) + varbytes(signature) # # Signing is randomized on purpose, so each run gives a different (but # valid) signature. The exam therefore checks that signatures are # ACCEPTED, not that they match byte for byte. TOOLS = "build/release/" # where the C++ tools were built def run_tool(name, lines): """ Send commands to one of the C++ tools and collect its answers. Each tool reads one command per line and writes one answer per line ("ERR" if it refuses). """ text = "\n".join(lines) + "\n" result = subprocess.run([TOOLS + name], input=text, capture_output=True, text=True) return result.stdout.splitlines() def address_payload(public_key): """ The lock for a one-owner slot (32 bytes), in pure Python: Refracting Light 128 of ("E46-address\\0" + key) 16 bytes + SHAKE256 128 of ("E46-address\\0" + key) 16 bytes Two different hash families side by side: an attacker must break both. """ label = b"E46-address\0" rl_part = refracting_light(label + public_key).to_bytes(16, "big") shake_part = hashlib.shake_256(label + public_key).digest(16) return rl_part + shake_part # Earlier version, using the C++ address tool (kept for comparison): # payload_hex = run_tool("addresses_cli", [f"single testnet {public_key.hex()}"])[0].split()[0] # return bytes.fromhex(payload_hex) def make_key(master_seed_hex, index): """ Make one real key pair, exactly as a wallet would (decision #18): master seed (32 bytes, your backup) -> KMAC256 -> 48-byte key seed for this index -> SLH-DSA-SHAKE-128s -> public key (32 B) + private key (64 B) Then work out its lock in pure Python (address_payload above). Returns (public_key, private_key, address_payload) as bytes. """ seed = run_tool("keys_cli", [f"seed {master_seed_hex} 0 {index}"])[0] public_hex, private_hex = run_tool("keys_cli", [f"keygen {seed}"])[0].split() public_key = bytes.fromhex(public_hex) return public_key, bytes.fromhex(private_hex), address_payload(public_key) def sign(private_key, message): """ Sign a 32-byte sighash (step 5) with a real SLH-DSA key. "-" means an empty context, the normal default (the bug fixed in WO-11). Returns the 7,856-byte signature. """ answer = run_tool("keys_cli", [f"sign {private_key.hex()} {message.hex()} -"])[0] return bytes.fromhex(answer) # --------------------------------------------------------------------------- # Step 7: THE EXAM # --------------------------------------------------------------------------- # # We build real, really-signed transactions with steps 1-6 and ask the C++ # checker (WO-12's transaction_cli "validate") to judge each one. # # test what we build C++ must say # A honest payment: 10.00 in, 7.00 + 2.99 out ACCEPT (the control) # A2 spend a coin worth exactly T, pay out exactly T ACCEPT (the limit itself is fine) # S A again, but one signature bit flipped REJECT (signatures are checked) # B1 coin worth T, one output of T + 1 REJECT (amount rule) # B2 coin worth T, outputs T and 1 (sum T + 1) REJECT (amount rule) # B3 coin worth 10.00, one output of 11.00 REJECT (outputs > inputs) # # Why this proves the amount rule did the rejecting: A and A2 pass with the # same keys, the same format and real signatures, and B1-B3 are signed for # real too. The ONLY thing that differs is the amounts. (Honest note: B1 and # B2 break two amount rules at once, "no more than T" and "no more out than # in", because no real coin can be worth more than T. Both are decision #20d; # the C++ tool says only ACCEPT or ERR, not which line fired.) T = 46_000_000 * 100_000_000 # total supply in base units: 46 million E46 E46 = 100_000_000 # 1 E46 in base units ONE_OWNER = 0x00 # output type 00: one owner NO_SPECIAL_RULES = 0xFFFFFFFF # sequence FF FF FF FF def build_signed(key, coin_value, outputs): """ Build and sign a 1-input transaction spending one coin locked to `key`. key (public_key, private_key, lock) from make_key coin_value how much the old slot holds, in base units outputs list of (value, type, payload) Returns (full transaction bytes, spent-coin description for the checker). """ public_key, private_key, lock = key old_txid = bytes([0xAB]) * 32 # pretend old transaction inputs = [(old_txid, 1, NO_SPECIAL_RULES)] # "slot 1 of AB...AB" spent = [(coin_value, ONE_OWNER, lock)] # what that slot holds base, _ = serialize_tx(1, inputs, outputs, 0, []) # no witness yet signature = sign(private_key, sighash(base, 0, spent)) witness = varbytes(public_key) + varbytes(signature) _, full = serialize_tx(1, inputs, outputs, 0, [witness]) # The checker's format for a spent coin: type:value:lock:is_coinbase:height descriptor = f"{ONE_OWNER}:{coin_value}:{lock.hex()}:0:1" return full, descriptor def judge(full, descriptor, height=500): """Ask the C++ checker. Returns "ACCEPT" or "REJECT".""" answer = run_tool("transaction_cli", [f"validate {full.hex()} {height} {descriptor}"])[0] return "ACCEPT" if answer == "1" else "REJECT" def run_exam(): me = make_key("11" * 32, 0) alice = make_key("22" * 32, 0) results = [] # A: honest payment full_a, desc_a = build_signed(me, 10 * E46, [(7 * E46, ONE_OWNER, alice[2]), (299_000_000, ONE_OWNER, me[2])]) results.append(("A honest payment 10.00 -> 7.00 + 2.99", "ACCEPT", judge(full_a, desc_a))) # A2: exactly T is allowed full, desc = build_signed(me, T, [(T, ONE_OWNER, alice[2])]) results.append(("A2 coin worth T, pay out exactly T", "ACCEPT", judge(full, desc))) # S: flip the last bit of the signature in A broken = bytearray(full_a) broken[-1] ^= 1 results.append(("S A with one signature bit flipped", "REJECT", judge(bytes(broken), desc_a))) # B1: one output of T + 1 full, desc = build_signed(me, T, [(T + 1, ONE_OWNER, alice[2])]) results.append(("B1 one output of T + 1", "REJECT", judge(full, desc))) # B2: two outputs, T and 1, summing to T + 1 full, desc = build_signed(me, T, [(T, ONE_OWNER, alice[2]), (1, ONE_OWNER, me[2])]) results.append(("B2 outputs T and 1 (sum T + 1)", "REJECT", judge(full, desc))) # B3: pays out more than it spends full, desc = build_signed(me, 10 * E46, [(11 * E46, ONE_OWNER, alice[2])]) results.append(("B3 10.00 in, 11.00 out", "REJECT", judge(full, desc))) passed = 0 for name, expected, got in results: ok = expected == got passed += ok print(f"{'PASS' if ok else 'FAIL'} {name:42} expected {expected:6} got {got}") print(f"\n{passed} of {len(results)} passed") return passed == len(results) def hexs(b, limit=48): """Show bytes as spaced hex; long data is cut in the middle.""" h = b.hex(" ").upper() if len(b) <= limit: return h return f"{b[:16].hex(' ').upper()} ... {b[-8:].hex(' ').upper()} ({len(b):,} bytes)" def debug(): """ DEBUG MODE: build test A step by step and print the raw bytes of every piece, so you can see exactly what each function produces. Run: python3 -I e46/tests/exam_wo12.py --debug """ print("STEP 1 compact_size") for n in (3, 252, 253, 7856): print(f" compact_size({n:>5}) -> {hexs(compact_size(n))}") print("\nSTEP 2 varbytes") print(f" varbytes(b'hi') -> {hexs(varbytes(b'hi'))}") print(f" varbytes(b'') -> {hexs(varbytes(b''))}") print("\nSTEP 3 tagged_hash") print(f" txid label, b'hi' -> {hexs(tagged_hash(b'E46-txid' + bytes(1), b'hi'))}") print(f" wtxid label, b'hi' -> {hexs(tagged_hash(b'E46-wtxid' + bytes(1), b'hi'))}") print("\nSTEP 6 keys and locks (made first: the outputs need the locks)") me = make_key("11" * 32, 0) alice = make_key("22" * 32, 0) print(f" my public key -> {hexs(me[0])}") print(f" my lock (address) -> {hexs(me[2])}") print(f" Alice's lock -> {hexs(alice[2])}") print("\nSTEP 4 the pieces of test A") inp = (bytes([0xAB]) * 32, 1, NO_SPECIAL_RULES) out1 = (7 * E46, ONE_OWNER, alice[2]) out2 = (299_000_000, ONE_OWNER, me[2]) print(f" input -> {hexs(serialize_input(*inp), 64)}") print(f" output 1 (7.00 Alice) -> {hexs(serialize_output(*out1), 64)}") print(f" output 2 (2.99 me) -> {hexs(serialize_output(*out2), 64)}") base, _ = serialize_tx(1, [inp], [out1, out2], 0, []) print(f" base (no witness) -> {len(base)} bytes") print(f" txid -> {hexs(txid(base))}") print("\nSTEP 5 sighash (what my key signs)") spent = [(10 * E46, ONE_OWNER, me[2])] msg = sighash(base, 0, spent) print(f" sighash -> {hexs(msg)}") print("\nSTEP 6 signing") sig = sign(me[1], msg) print(f" signature -> {hexs(sig)}") witness = varbytes(me[0]) + varbytes(sig) print(f" witness -> {hexs(witness)}") print("\nSTEP 4 the full transaction") _, full = serialize_tx(1, [inp], [out1, out2], 0, [witness]) print(f" full -> {len(full):,} bytes (base {len(base)} + witness {len(witness):,})") print(f" txid (same as before)-> {hexs(txid(base))}") print(f" wtxid -> {hexs(wtxid(full))}") print("\nRAW TRANSACTION, LAID OUT (first 138 bytes = the base)") pos = 0 for label, size in [("version", 4), ("input count", 1), ("input: prev_txid", 32), ("input: prev_index", 4), ("input: sequence", 4), ("output count", 1), ("out1: value", 8), ("out1: type", 1), ("out1: lock length", 1), ("out1: lock", 32), ("out2: value", 8), ("out2: type", 1), ("out2: lock length", 1), ("out2: lock", 32), ("lock_time", 8), ("wit: key length", 1), ("wit: public key", 32), ("wit: sig length", 3), ("wit: signature", 7856)]: print(f" {pos:>5} {label:18} {hexs(full[pos:pos + size])}") pos += size print(f" {pos:>5} end") print("\nTHE CHECKER'S VERDICT") print(f" test A -> {judge(*build_signed(me, 10 * E46, [out1, out2]))}") if __name__ == "__main__": if "--debug" in sys.argv: debug() sys.exit(0) sys.exit(0 if run_exam() else 1)