|
59 | 59 | ) |
60 | 60 |
|
61 | 61 |
|
| 62 | +# Full BIP-144 serializations, captured from the emulator and cross-checked |
| 63 | +# against an independent derivation from this file's own inputs and the |
| 64 | +# EXPECTED_* witnesses above. These pin the bytes the host would broadcast -- |
| 65 | +# `signature` alone was populated correctly even while the witness and the |
| 66 | +# locktime footer were being dropped on the wire. |
| 67 | +EXPECTED_SERIALIZED_TX = ( |
| 68 | + "0100000000010137eea6e08b6227cd775f08153e291187d0df2a23261dab50752f98" |
| 69 | + "113903326e0000000000ffffffff01905f0100000000001976a914759d6677091e97" |
| 70 | + "3b9e9d99f19c68fbf43e3f05f988ac0140afe221b16d648a1ad7329f976593073238" |
| 71 | + "0cc67765bd73af7ce13b59911468512d9ee77e34af56fe1f59f98372011f7cb400ce" |
| 72 | + "d614d808c690c5ba907fb62de900000000" |
| 73 | +) |
| 74 | +EXPECTED_SERIALIZED_TX_CHANGE = ( |
| 75 | + "0100000000010137eea6e08b6227cd775f08153e291187d0df2a23261dab50752f98" |
| 76 | + "113903326e0000000000ffffffff0250c30000000000001976a914759d6677091e97" |
| 77 | + "3b9e9d99f19c68fbf43e3f05f988ac409c000000000000225120882d74e5d0572d5a" |
| 78 | + "816cef0041a96b6c1de832f6f9676d9605c44d5e9a97d3dc0140e3c44408fe61256a" |
| 79 | + "d406733f100f1ee856eb31854335efa59e60a61ea5d41ab341802f0cccb55f644042" |
| 80 | + "a1ab390f0a406b9d3efe3996d05442b4ee43d5355eab00000000" |
| 81 | +) |
| 82 | +EXPECTED_SERIALIZED_TX_MIXED = ( |
| 83 | + "01000000000102a4a9ecee1384341b77c2db4d5cc54239854f0efc5f9978f3a2a878" |
| 84 | + "2608df1f3e0000000000ffffffffa4a9ecee1384341b77c2db4d5cc54239854f0efc" |
| 85 | + "5f9978f3a2a8782608df1f3e010000006a47304402205aa50469308c21e9e1ba0299" |
| 86 | + "cd235add026914e4406bcfa6d9c0403c8cc3cf580220764a5832ad1bc36ba6a21020" |
| 87 | + "a253c2272bca5aa1643d9c41b12c318b0a38824e012103aaeb52dd7494c361049de6" |
| 88 | + "7cc680e83ebcbbbdbeb13637d92cd845f70308af5effffffff01e022020000000000" |
| 89 | + "1976a914759d6677091e973b9e9d99f19c68fbf43e3f05f988ac0140b596e1bbefb8" |
| 90 | + "55af9852942797075d4f452b2d186cb17a76226892334a497a62adb9a02f7c1b4573" |
| 91 | + "e4d48b92e2307bb0b2282c97e2c5350bb3c21619fab855a20000000000" |
| 92 | +) |
| 93 | + |
| 94 | + |
62 | 95 | class TestMsgSigntxTaproot(KeepKeyTest): |
63 | 96 |
|
| 97 | + def assertCompleteSegwitTx(self, raw, signatures, n_in, n_out): |
| 98 | + """Parse the serialized tx strictly; it must consume exactly len(raw). |
| 99 | +
|
| 100 | + `signature` and `serialized_tx` are separate nanopb fields on |
| 101 | + TxRequestSerializedType, each with its own presence flag. Asserting |
| 102 | + only `signature` passes even when the device never transmits the |
| 103 | + witness stack -- the host then gets a tx that declares the segwit |
| 104 | + marker/flag, carries no witness and no locktime, and every node |
| 105 | + rejects it. A structural parse catches that: the marker promises |
| 106 | + witnesses, so the stream ends early and the offset check fails. |
| 107 | +
|
| 108 | + Returns the witness stacks, one list per input. |
| 109 | + """ |
| 110 | + pos = [0] |
| 111 | + |
| 112 | + def take(n): |
| 113 | + if len(raw) < pos[0] + n: |
| 114 | + raise AssertionError( |
| 115 | + "tx truncated at offset %d: wanted %d more byte(s) of %d " |
| 116 | + "total: %s" |
| 117 | + % (pos[0], n, len(raw), hexlify(raw).decode())) |
| 118 | + out = raw[pos[0]:pos[0] + n] |
| 119 | + pos[0] += n |
| 120 | + return out |
| 121 | + |
| 122 | + def varint(): |
| 123 | + first = take(1)[0] |
| 124 | + if first < 0xfd: |
| 125 | + return first |
| 126 | + width = {0xfd: 2, 0xfe: 4, 0xff: 8}[first] |
| 127 | + return int.from_bytes(take(width), "little") |
| 128 | + |
| 129 | + take(4) # nVersion |
| 130 | + marker = take(2) |
| 131 | + if marker != unhexlify("0001"): |
| 132 | + raise AssertionError( |
| 133 | + "missing segwit marker/flag: got %s" % hexlify(marker).decode()) |
| 134 | + if varint() != n_in: |
| 135 | + raise AssertionError("unexpected input count") |
| 136 | + for _ in range(n_in): |
| 137 | + take(32); take(4); take(varint()); take(4) # outpoint, sig, seq |
| 138 | + if varint() != n_out: |
| 139 | + raise AssertionError("unexpected output count") |
| 140 | + for _ in range(n_out): |
| 141 | + take(8); take(varint()) # value, scriptPubKey |
| 142 | + witnesses = [[take(varint()) for _ in range(varint())] |
| 143 | + for _ in range(n_in)] |
| 144 | + take(4) # nLockTime footer |
| 145 | + if pos[0] != len(raw): |
| 146 | + raise AssertionError( |
| 147 | + "trailing bytes: parsed %d of %d" % (pos[0], len(raw))) |
| 148 | + |
| 149 | + # Every BIP-340 signature the device reported must actually appear in |
| 150 | + # the witness data it serialized. |
| 151 | + flat = [item for stack in witnesses for item in stack] |
| 152 | + for sig in signatures: |
| 153 | + if len(sig) == 64 and sig not in flat: |
| 154 | + raise AssertionError( |
| 155 | + "schnorr signature absent from serialized_tx witnesses") |
| 156 | + return witnesses |
| 157 | + |
64 | 158 | def test_send_p2tr(self): |
65 | 159 | """Spend a P2TR input and compare the witness byte for byte. |
66 | 160 |
|
@@ -115,11 +209,15 @@ def test_send_p2tr(self): |
115 | 209 | request_index=0)), |
116 | 210 | proto.TxRequest(request_type=proto_types.TXFINISHED), |
117 | 211 | ]) |
118 | | - (signatures, _) = self.client.sign_tx( |
| 212 | + (signatures, serialized) = self.client.sign_tx( |
119 | 213 | "Bitcoin", [inp1], [out1]) |
120 | 214 |
|
121 | 215 | self.assertEqual(len(signatures), 1) |
122 | 216 | self.assertEqual(hexlify(signatures[0]).decode(), EXPECTED_WITNESS) |
| 217 | + witnesses = self.assertCompleteSegwitTx(serialized, signatures, 1, 1) |
| 218 | + # key-path spend: exactly one stack item, the bare 64-byte signature |
| 219 | + self.assertEqual(witnesses[0], [signatures[0]]) |
| 220 | + self.assertEqual(hexlify(serialized).decode(), EXPECTED_SERIALIZED_TX) |
123 | 221 |
|
124 | 222 | def test_send_p2tr_with_change(self): |
125 | 223 | """P2TR change is device-derived and omitted from recipient prompts.""" |
@@ -150,7 +248,14 @@ def test_send_p2tr_with_change(self): |
150 | 248 |
|
151 | 249 | self.assertEqual(hexlify(signatures[0]).decode(), |
152 | 250 | EXPECTED_CHANGE_WITNESS) |
| 251 | + # EXPECTED_CHANGE_SCRIPT is a phase-1 output byte, which the device |
| 252 | + # transmits regardless of whether the witness ever reaches the host. |
| 253 | + # Assert the whole transaction, not just that prefix. |
153 | 254 | self.assertIn(unhexlify(EXPECTED_CHANGE_SCRIPT), serialized) |
| 255 | + witnesses = self.assertCompleteSegwitTx(serialized, signatures, 1, 2) |
| 256 | + self.assertEqual(witnesses[0], [signatures[0]]) |
| 257 | + self.assertEqual(hexlify(serialized).decode(), |
| 258 | + EXPECTED_SERIALIZED_TX_CHANGE) |
154 | 259 |
|
155 | 260 | def test_send_mixed_p2tr_and_legacy(self): |
156 | 261 | """A P2TR signature commits to the legacy input's real prevout.""" |
@@ -178,13 +283,19 @@ def test_send_mixed_p2tr_and_legacy(self): |
178 | 283 | script_type=proto_types.PAYTOADDRESS, |
179 | 284 | ) |
180 | 285 |
|
181 | | - (signatures, _) = self.client.sign_tx( |
| 286 | + (signatures, serialized) = self.client.sign_tx( |
182 | 287 | "Bitcoin", [taproot, legacy], [recipient]) |
183 | 288 |
|
184 | 289 | self.assertEqual(len(signatures), 2) |
185 | 290 | self.assertEqual(hexlify(signatures[0]).decode(), |
186 | 291 | EXPECTED_MIXED_WITNESS) |
187 | 292 | self.assertTrue(signatures[1]) |
| 293 | + witnesses = self.assertCompleteSegwitTx(serialized, signatures, 2, 1) |
| 294 | + self.assertEqual(witnesses[0], [signatures[0]]) |
| 295 | + # the legacy input must still serialize an EMPTY witness (0x00) |
| 296 | + self.assertEqual(witnesses[1], []) |
| 297 | + self.assertEqual(hexlify(serialized).decode(), |
| 298 | + EXPECTED_SERIALIZED_TX_MIXED) |
188 | 299 |
|
189 | 300 | def test_mixed_p2tr_requires_every_input_amount(self): |
190 | 301 | """Fail closed instead of signing an incomplete BIP-341 commitment.""" |
|
0 commit comments