unit-tests: drive the BIP-322 tests from the BIP's pinned test vectors
What changed, and why it matters
This commit is a routine improvement to the project's automated tests. It replaces two hand-copied BIP-322 test cases with a larger set of official, pinned test vectors (including a new UTF-8 message case) and adds a code generator plus CI check to keep the generated test data in sync. It does not change the actual Bitcoin app code that users run, so it cannot directly affect device security or user funds.
No security action required. Review the generator and CI check as normal code-quality/test-coverage improvements if desired.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change is entirely in the unit-test and build/CI layer. It vendors the upstream BIP-0322 basic-test-vectors.json, adds specs/bip322/gen.py to regenerate unit-tests/bip322_vectors.inc.c, extends the GitHub Actions workflow to verify the generated file is in sync, and updates test_bip322.c to drive tests from the generated vector table. The app implementation in src/common/bip322.c is only exercised, not modified. No runtime behavior of the Ledger app changes.
Changed components
unit-tests/test_bip322.cunit-tests/bip322_vectors.inc.cunit-tests/CMakeLists.txt.github/workflows/codegen-check.ymlspecs/bip322/basic-test-vectors.jsonspecs/bip322/gen.pyspecs/bip322/README.mdInspect captured patch +874 / −89
### .github/workflows/codegen-check.yml
@@ -1,9 +1,11 @@
-name: Cleartext codegen check
+name: Codegen check
-# Ensures the generated BIP388 cleartext files committed to the repo are in sync
-# with the TOML specs they are derived from. If someone edits
-# specs/bip388/*.toml without re-running specs/bip388/gen.py, this
-# job fails and points at the drifted files.
+# Ensures the generated files committed to the repo are in sync with the specs
+# they are derived from:
+# - the BIP388 cleartext files, generated from specs/bip388/*.toml
+# - the BIP-322 unit-test vectors, generated from specs/bip322/basic-test-vectors.json
+# If someone edits a spec without re-running the corresponding gen.py, this job
+# fails and points at the drifted files.
on:
workflow_dispatch:
@@ -30,3 +32,6 @@ jobs:
- name: Check codegen is in sync with the TOML specs
run: python3 specs/bip388/gen.py --check
+
+ - name: Check the BIP-322 unit-test vectors are in sync with the pinned JSON
+ run: python3 specs/bip322/gen.py --check
### specs/bip322/README.md
@@ -0,0 +1,43 @@
+# BIP-322 test vectors
+
+This folder holds a **pinned, verbatim copy** of the test vectors published with
+[BIP-0322](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki), and the
+generator that turns them into a C array for the unit tests.
+
+## Files
+
+| File | Role |
+|---------------------------|------|
+| `basic-test-vectors.json` | Byte-for-byte copy of `bip-0322/basic-test-vectors.json` from the bips repository. Never edit it by hand: replace it with a newer upstream copy and update the pin below. |
+| `gen.py` | Code generator. Reads the `tx_hashes` table of the JSON, independently recomputes every hash (as a guard against misreading the file), and (over)writes the generated C file listed below, or verifies that it is up to date. |
+
+## Pin
+
+| | |
+|---|---|
+| Upstream commit | [`d77863fb9e9be7829ad8bb51694b9ba80a786766`](https://github.com/bitcoin/bips/blob/d77863fb9e9be7829ad8bb51694b9ba80a786766/bip-0322/basic-test-vectors.json) (2026-05-06, "BIP-0322: update test vectors") |
+| BIP-322 version | 2.0.0 |
+| sha256 | `a9184da5687b9cb7a1863807f4a8f4d743ec07d4f683f707cb85ce72f5b33f47` |
+
+## Generated files
+
+`gen.py` overwrites this file; it is committed to the repo but must never be edited
+by hand (it carries a `// Generated by specs/bip322/gen.py. DO NOT EDIT.` banner):
+
+- `unit-tests/bip322_vectors.inc.c` — the `tx_hashes` vectors as a static C array
+ (`BIP322_TX_HASHES_VECTORS`), consumed by `unit-tests/test_bip322.c`.
+
+The `simple` and `error` tables of the JSON (complete signatures for given private
+keys, and malformed signatures) are about signature verification, which the app does
+not do; they are not consumed.
+
+## Updating the vectors
+
+```
+curl -sL https://raw.githubusercontent.com/bitcoin/bips/<commit>/bip-0322/basic-test-vectors.json \
+ -o specs/bip322/basic-test-vectors.json
+python3 specs/bip322/gen.py
+```
+
+then update the pin table above and commit everything. CI runs
+`python3 specs/bip322/gen.py --check` and fails if the generated file drifted.
### specs/bip322/basic-test-vectors.json
@@ -0,0 +1,137 @@
+{
+ "tx_hashes": [
+ {
+ "message": "",
+ "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
+ "message_hash": "c90c269c4f8fcbe6880f72a721ddfbf1914268a794cbb21cfafee13770ae19f1",
+ "to_spend_tx_hash": "c5680aa69bb8d860bf82d4e9cd3504b55dde018de765a91bb566283c545a99a7",
+ "to_sign_tx_hash": "1e9654e951a5ba44c8604c4de6c67fd78a27e81dcadcfe1edf638ba3aaebaed6"
+ },
+ {
+ "message": "Hello World",
+ "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
+ "message_hash": "f0eb03b1a75ac6d9847f55c624a99169b5dccba2a31f5b23bea77ba270de0a7a",
+ "to_spend_tx_hash": "b79d196740ad5217771c1098fc4a4b51e0535c32236c71f1ea4d61a2d603352b",
+ "to_sign_tx_hash": "88737ae86f2077145f93cc4b153ae9a1cb8d56afa511988c149c5c8c9d93bddf"
+ },
+ {
+ "message": "UTF-8 support: öäüéàè 测试文本 \uD83D\uDE04",
+ "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
+ "message_hash": "43936b237ea38c7794eb5d755e0d220b6db92ebfc5c8f482759d22b1286376d7",
+ "to_spend_tx_hash": "c8f4f525fe8afb1bc09b44175bd2096f079c98425e8a1be676b712add1fb62f0",
+ "to_sign_tx_hash": "8f488e06b89eafd019ec528109eafaf7f1d1811fd617aa1eeb9658f1c1be6586"
+ }
+ ],
+ "simple": [
+ {
+ "message": "",
+ "private_keys": [
+ "L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k"
+ ],
+ "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
+ "type": "p2wpkh",
+ "witness_script": "",
+ "bip322_signatures": [
+ "smpAkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=",
+ "smpAkgwRQIhAPkJ1Q4oYS0htvyuSFHLxRQpFAY56b70UvE7Dxazen0ZAiAtZfFz1S6T6I23MWI2lK/pcNTWncuyL8UL+oMdydVgzAEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy"
+ ]
+ },
+ {
+ "message": "Hello World",
+ "private_keys": [
+ "L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k"
+ ],
+ "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
+ "type": "p2wpkh",
+ "witness_script": "",
+ "bip322_signatures": [
+ "smpAkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=",
+ "smpAkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy"
+ ]
+ },
+ {
+ "message": "This will be a p2wsh 3-of-3 multisig BIP 322 signed message",
+ "private_keys": [
+ "L4DksdGZ4KQJfcLHD5Dv25fu8Rxyv7hHi2RjZR4TYzr8c6h9VNrp",
+ "KzSRqnCVwjzY8id2X5oHEJWXkSHwKUYaAXusjwgkES8BuQPJnPNu",
+ "L1zt9Rw7HrU7jaguMbVzhiX8ffuVkmMis5wLHddXYuHWYf8u8uRj"
+ ],
+ "address": "bc1qp0ahvfh83088w49k405szqgg4f3pptr7p2g06tdxfjcd40z4lh4q95lsz9",
+ "type": "p2wsh-multisig-3of3",
+ "witness_script": "5321027568b11f122ff8a7bc1c57e5c7642055bc618967b2f7bfe8e11fe99903c94dd321020a8bdf79cfa421d9655e9282800f115ff1d9db1e721ceb4248a3fcfec7faa67c21030c529e0ea40a00975d202624e39915daf7bdd2b71f31aa08596838781ce5f33a53ae",
+ "bip322_signatures": [
+ "smpBQBHMEQCIFX9aaqPJWq2Ff2kpen5bFDTid+ehgUOpHV0LfjncXy4AiA3GNicF7aKPzdpa9PCpmaYQs3pHd+qbvvhXdxOCKCAMAFIMEUCIQD/ELXg6CNYyUQijCg96JtgvgjZb9dsl1Ctof4QAeyTcQIgVM/1AAblFl/DCt6A1gJg+T/i2qU5SQD09+chFJzolRwBSDBFAiEAlqRfSFyWNVQhvaCnmeV5tyneiCWMTcFbuujoD/pFa3wCIGnZjfQb8NolSYq9asV+ZeBSkCGHJcqnaV4JYS5MYPEGAWlTIQJ1aLEfEi/4p7wcV+XHZCBVvGGJZ7L3v+jhH+mZA8lN0yECCovfec+kIdllXpKCgA8RX/HZ2x5yHOtCSKP8/sf6pnwhAwxSng6kCgCXXSAmJOOZFdr3vdK3HzGqCFloOHgc5fM6U64="
+ ]
+ },
+ {
+ "message": "No prefix fallback",
+ "private_keys": [
+ "KyrSGCFPhqZMjCe5fNTYddiLMp4tMj4gLKuJ26TsB2rvr1VJGPbt"
+ ],
+ "address": "bc1pss0zhytly75awhm6x2hhvd5lnzv3vssgrf9axfheq8ldyzn88ges79fler",
+ "type": "p2tr",
+ "witness_script": "",
+ "bip322_signatures": [
+ "AUCJYOwOjxYAvatTAGYaVlNXBVyFuc4MwNQkOuK2tl8xhfKDONd0NjfYyNSYcRqeCp8hsAnCEPHAVEkO9h6vbQ/R"
+ ]
+ }
+ ],
+ "error": [
+ {
+ "description": "invalid base64 encoding",
+ "message": "",
+ "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
+ "signature": "not-valid-base64!!!",
+ "error_substr": "base64"
+ },
+ {
+ "description": "empty signature",
+ "message": "",
+ "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
+ "signature": "",
+ "error_substr": "signature too short"
+ },
+ {
+ "description": "wrong message for valid simple p2wpkh signature (empty message was signed)",
+ "message": "Wrong message that was not signed",
+ "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
+ "signature": "smpAkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=",
+ "error_substr": "invalid signature"
+ },
+ {
+ "description": "wrong address for valid simple p2wpkh signature (signed for different address)",
+ "message": "",
+ "address": "bc1qp0ahvfh83088w49k405szqgg4f3pptr7p2g06tdxfjcd40z4lh4q95lsz9",
+ "signature": "smpAkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=",
+ "error_substr": "invalid signature"
+ },
+ {
+ "description": "empty witness stack (single zero byte)",
+ "message": "",
+ "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
+ "signature": "smpAA==",
+ "error_substr": "invalid signature"
+ },
+ {
+ "description": "wrong message for valid simple p2wsh 3-of-3 multisig signature",
+ "message": "This is not the message that was signed",
+ "address": "bc1qp0ahvfh83088w49k405szqgg4f3pptr7p2g06tdxfjcd40z4lh4q95lsz9",
+ "signature": "smpBQBHMEQCIFX9aaqPJWq2Ff2kpen5bFDTid+ehgUOpHV0LfjncXy4AiA3GNicF7aKPzdpa9PCpmaYQs3pHd+qbvvhXdxOCKCAMAFIMEUCIQD/ELXg6CNYyUQijCg96JtgvgjZb9dsl1Ctof4QAeyTcQIgVM/1AAblFl/DCt6A1gJg+T/i2qU5SQD09+chFJzolRwBSDBFAiEAlqRfSFyWNVQhvaCnmeV5tyneiCWMTcFbuujoD/pFa3wCIGnZjfQb8NolSYq9asV+ZeBSkCGHJcqnaV4JYS5MYPEGAWlTIQJ1aLEfEi/4p7wcV+XHZCBVvGGJZ7L3v+jhH+mZA8lN0yECCovfec+kIdllXpKCgA8RX/HZ2x5yHOtCSKP8/sf6pnwhAwxSng6kCgCXXSAmJOOZFdr3vdK3HzGqCFloOHgc5fM6U64=",
+ "error_substr": "invalid signature"
+ },
+ {
+ "description": "invalid signature prefix",
+ "message": "",
+ "address": "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
+ "signature": "fooAA==",
+ "error_substr": "error decoding signature as base64"
+ },
+ {
+ "description": "incorrect prefix type",
+ "message": "incorrect prefix",
+ "address": "bc1pyrgrm6cu6n54jrvkdjd9rvyd3xfyu84s2623awu2srn6mxhscwpsm5644w",
+ "signature": "fulAUDZwFXUp+adN+/UZj5dVrGAbB3zKs1Vcalz5fCF9srxS63eSWNGvH1NYbrBkPt1BJDUyWUz9zgUxfc63/QheT6M",
+ "error_substr": "error parsing signature as full variant"
+ }
+ ]
+}
\ No newline at end of file
### specs/bip322/gen.py
@@ -0,0 +1,324 @@
+#!/usr/bin/env python3
+"""Generate the C unit-test vector array from the pinned BIP-322 test vectors.
+
+Input:
+ specs/bip322/basic-test-vectors.json -- verbatim copy of bip-0322/basic-test-vectors.json
+
+Output (overwritten):
+ unit-tests/bip322_vectors.inc.c -- static C array of the tx_hashes vectors
+
+Only the "tx_hashes" table is consumed: for each (message, address) pair it gives the
+BIP0322-signed-message tagged hash of the message and the txids of the to_spend and
+to_sign virtual transactions. Every hash is recomputed here and compared with the JSON
+before emitting anything, so that a byte-order or encoding misreading of the file cannot
+silently turn into a wrong expected value.
+
+Run this script manually after replacing the JSON and commit the result. Pass --check
+(e.g. in CI) to verify the committed output is up to date instead of regenerating it.
+"""
+
+from __future__ import annotations
+
+import argparse
+import difflib
+import json
+import struct
+import sys
+from hashlib import sha256
+from pathlib import Path
+
+SPECS_DIR = Path(__file__).resolve().parent
+REPO_ROOT = SPECS_DIR.parents[1]
+VECTORS_FILE = SPECS_DIR / "basic-test-vectors.json"
+VECTORS_OUT = REPO_ROOT / "unit-tests" / "bip322_vectors.inc.c"
+
+BIP0322_TAG = b"BIP0322-signed-message"
+
+
+# ---------------------------------------------------------------------------
+# bech32 / bech32m (BIP-173 / BIP-350) address decoding
+# ---------------------------------------------------------------------------
+
+_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
+_BECH32_CONST = 1
+_BECH32M_CONST = 0x2BC830A3
+
+
+def _bech32_polymod(values: list[int]) -> int:
+ generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
+ chk = 1
+ for value in values:
+ top = chk >> 25
+ chk = (chk & 0x1FFFFFF) << 5 ^ value
+ for i in range(5):
+ chk ^= generator[i] if ((top >> i) & 1) else 0
+ return chk
+
+
+def _bech32_hrp_expand(hrp: str) -> list[int]:
+ return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
+
+
+def _convertbits(data: list[int], frombits: int, tobits: int) -> list[int]:
+ acc = 0
+ bits = 0
+ ret = []
+ maxv = (1 << tobits) - 1
+ for value in data:
+ acc = (acc << frombits) | value
+ bits += frombits
+ while bits >= tobits:
+ bits -= tobits
+ ret.append((acc >> bits) & maxv)
+ if bits >= frombits or ((acc << (tobits - bits)) & maxv):
+ raise ValueError("invalid padding in bech32 data")
+ return ret
+
+
+def address_to_script(address: str) -> bytes:
+ """Decodes a segwit (bech32/bech32m) address into its scriptPubKey."""
+ if address.lower() != address and address.upper() != address:
+ raise ValueError(f"mixed-case address: {address}")
+ address = address.lower()
+ hrp, sep, data_part = address.rpartition("1")
+ if not sep or hrp not in ("bc", "tb", "bcrt"):
+ raise ValueError(f"unsupported address (only segwit addresses are handled): {address}")
+ data = [_CHARSET.find(c) for c in data_part]
+ if -1 in data:
+ raise ValueError(f"invalid bech32 character in {address}")
+ const = _bech32_polymod(_bech32_hrp_expand(hrp) + data)
+ witness_version = data[0]
+ program = bytes(_convertbits(data[1:-6], 5, 8))
+ expected_const = _BECH32_CONST if witness_version == 0 else _BECH32M_CONST
+ if const != expected_const:
+ raise ValueError(f"bad checksum for {address}")
+ if not 2 <= len(program) <= 40:
+ raise ValueError(f"bad witness program length for {address}")
+ op = 0 if witness_version == 0 else 0x50 + witness_version
+ return bytes([op, len(program)]) + program
+
+
+# ---------------------------------------------------------------------------
+# BIP-322 virtual transactions (independent reimplementation, used to
+# cross-check the JSON before emitting it)
+# ---------------------------------------------------------------------------
+
+def tagged_hash(tag: bytes, message: bytes) -> bytes:
+ tag_hash = sha256(tag).digest()
+ return sha256(tag_hash + tag_hash + message).digest()
+
+
+def sha256d(data: bytes) -> bytes:
+ return sha256(sha256(data).digest()).digest()
+
+
+def to_spend_tx(message_hash: bytes, challenge_script: bytes) -> bytes:
+ return b"".join([
+ struct.pack("<i", 0), # nVersion
+ b"\x01", # 1 input
+ b"\x00" * 32, struct.pack("<I", 0xFFFFFFFF), # null outpoint
+ b"\x22\x00\x20" + message_hash, # scriptSig: OP_0 PUSH32(message_hash)
+ struct.pack("<I", 0), # nSequence
+ b"\x01", # 1 output
+ struct.pack("<q", 0), # value
+ bytes([len(challenge_script)]) + challenge_script,
+ struct.pack("<I", 0), # nLockTime
+ ])
+
+
+def to_sign_tx(to_spend_txid: bytes) -> bytes:
+ return b"".join([
+ struct.pack("<i", 0), # nVersion
+ b"\x01", # 1 input
+ to_spend_txid, struct.pack("<I", 0), # spends to_spend:0
+ b"\x00", # empty scriptSig
+ struct.pack("<I", 0), # nSequence
+ b"\x01", # 1 output
+ struct.pack("<q", 0), # value
+ b"\x01\x6a", # OP_RETURN
+ struct.pack("<I", 0), # nLockTime
+ ])
+
+
+# ---------------------------------------------------------------------------
+# Loading and cross-checking
+# ---------------------------------------------------------------------------
+
+def load_tx_hashes_vectors() -> list[dict]:
+ """Returns the tx_hashes vectors, with all byte fields decoded into bytes objects
+ (txids in the internal byte order used in transaction serializations and in
+ PSBT_IN_PREVIOUS_TXID), after checking them against an independent computation."""
+ doc = json.loads(VECTORS_FILE.read_text(encoding="utf-8"))
+ vectors = []
+ for i, entry in enumerate(doc["tx_hashes"]):
+ message = entry["message"].encode("utf-8")
+ script = address_to_script(entry["address"])
+ message_hash = bytes.fromhex(entry["message_hash"])
+ # txids are displayed in reverse byte order
+ to_spend_txid = bytes.fromhex(entry["to_spend_tx_hash"])[::-1]
+ to_sign_txid = bytes.fromhex(entry["to_sign_tx_hash"])[::-1]
+
+ computed_message_hash = tagged_hash(BIP0322_TAG, message)
+ if computed_message_hash != message_hash:
+ raise SystemExit(f"tx_hashes[{i}]: message_hash does not match the tagged hash")
+ computed_to_spend_txid = sha256d(to_spend_tx(message_hash, script))
+ if computed_to_spend_txid != to_spend_txid:
+ raise SystemExit(f"tx_hashes[{i}]: to_spend_tx_hash does not match")
+ if sha256d(to_sign_tx(to_spend_txid)) != to_sign_txid:
+ raise SystemExit(f"tx_hashes[{i}]: to_sign_tx_hash does not match")
+
+ vectors.append({
+ "message": message,
+ "address": entry["address"],
+ "script": script,
+ "message_hash": message_hash,
+ "to_spend_txid": to_spend_txid,
+ "to_sign_txid": to_sign_txid,
+ })
+ return vectors
+
+
+# ---------------------------------------------------------------------------
+# C emission
+# ---------------------------------------------------------------------------
+
+def c_bytes(data: bytes, indent: str = " ") -> str:
+ if not data:
+ return indent + "0" # placeholder; the length field is what matters
+ lines = []
+ for off in range(0, len(data), 16):
+ chunk = data[off:off + 16]
+ lines.append(indent + ", ".join(f"0x{b:02x}" for b in chunk) + ",")
+ return "\n".join(lines)
+
+
+def c_string(data: bytes) -> str:
+ """A C string literal for the (UTF-8) message, escaping everything outside printable
+ ASCII as octal escapes (which cannot swallow the following characters, unlike \\x)."""
+ out = []
+ for b in data:
+ if b == 0x22:
+ out.append('\\"')
+ elif b == 0x5C:
+ out.append("\\\\")
+ elif 0x20 <= b <= 0x7E:
+ out.append(chr(b))
+ else:
+ out.append(f"\\{b:03o}")
+ return '"' + "".join(out) + '"'
+
+
+def emit_vectors(vectors: list[dict]) -> str:
+ out = [
+ "// Generated by specs/bip322/gen.py. DO NOT EDIT.",
+ "// Source: specs/bip322/basic-test-vectors.json (the \"tx_hashes\" table), a pinned",
+ "// copy of bip-0322/basic-test-vectors.json from the bips repository.",
+ "// clang-format off",
+ "",
+ "typedef struct {",
+ " const char *message_str; // the message, as a C string literal (for test output)",
+ " const uint8_t *message; // the UTF-8 encoded message",
+ " size_t message_len;",
+ " const char *address;",
+ " const uint8_t *challenge_script; // scriptPubKey of address",
+ " size_t challenge_script_len;",
+ " uint8_t message_hash[32]; // BIP0322-signed-message tagged hash of message",
+ " uint8_t to_spend_txid[32]; // txid of to_spend, in internal byte order",
+ " uint8_t to_sign_txid[32]; // txid of to_sign, in internal byte order",
+ "} bip322_tx_hashes_vector_t;",
+ "",
+ ]
+ for i, v in enumerate(vectors):
+ out.append(f"static const uint8_t vec_{i:03d}_message[] = {{")
+ out.append(c_bytes(v["message"]))
+ out.append("};")
+ out.append(f"static const uint8_t vec_{i:03d}_script[] = {{")
+ out.append(c_bytes(v["script"]))
+ out.append("};")
+ out.append("")
+
+ out.append("static const bip322_tx_hashes_vector_t BIP322_TX_HASHES_VECTORS[] = {")
+ for i, v in enumerate(vectors):
+ out.append(" {")
+ out.append(f" .message_str = {c_string(v['message'])},")
+ out.append(f" .message = vec_{i:03d}_message,")
+ out.append(f" .message_len = {len(v['message'])},")
+ out.append(f" .address = \"{v['address']}\",")
+ out.append(f" .challenge_script = vec_{i:03d}_script,")
+ out.append(f" .challenge_script_len = {len(v['script'])},")
+ for field in ("message_hash", "to_spend_txid", "to_sign_txid"):
+ out.append(f" .{field} = {{")
+ out.append(c_bytes(v[field], indent=" "))
+ out.append(" },")
+ out.append(" },")
+ out.append("};")
+ out.append("")
+ out.append("#define BIP322_TX_HASHES_VECTORS_COUNT "
+ "(sizeof(BIP322_TX_HASHES_VECTORS) / sizeof(BIP322_TX_HASHES_VECTORS[0]))")
+ return "\n".join(out)
+
+
+# ---------------------------------------------------------------------------
+# Driver
+# ---------------------------------------------------------------------------
+
+def generate_outputs() -> list[tuple[Path, str]]:
+ return [(VECTORS_OUT, emit_vectors(load_tx_hashes_vectors()) + "\n")]
+
+
+def write_outputs(outputs: list[tuple[Path, str]]) -> int:
+ for path, content in outputs:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(content)
+ print(f"wrote {path.relative_to(REPO_ROOT)}")
+ return 0
+
+
+def check_outputs(outputs: list[tuple[Path, str]]) -> int:
+ stale: list[Path] = []
+ for path, content in outputs:
+ rel = path.relative_to(REPO_ROOT)
+ current = path.read_text() if path.exists() else None
+ if current == content:
+ print(f"ok {rel}")
+ continue
+ stale.append(rel)
+ if current is None:
+ print(f"MISSING {rel} (file does not exist)")
+ continue
+ print(f"DRIFT {rel}")
+ sys.stdout.writelines(difflib.unified_diff(
+ current.splitlines(keepends=True),
+ content.splitlines(keepends=True),
+ fromfile=f"{rel} (committed)",
+ tofile=f"{rel} (generated)",
+ ))
+ if stale:
+ print()
+ print(f"error: {len(stale)} generated file(s) are out of date; regenerate with:")
+ print(" python3 specs/bip322/gen.py")
+ return 1
+ print("All generated files are up to date.")
+ return 0
+
+
+def main(argv: list[str] | None = None) -> int:
+ parser = argparse.ArgumentParser(
+ prog="gen.py",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ description=__doc__,
+ )
+ parser.add_argument(
+ "-c", "--check", action="store_true",
+ help="don't write anything; verify the committed file matches what this script "
+ "would generate and exit non-zero on any drift.",
+ )
+ args = parser.parse_args(argv)
+ outputs = generate_outputs()
+ if args.check:
+ return check_outputs(outputs)
+ return write_outputs(outputs)
+
+
+if __name__ == "__main__":
+ sys.exit(main())
### unit-tests/CMakeLists.txt
@@ -333,7 +333,8 @@ if(SPECULOS AND SPECULOS_SRC)
add_test(test_script test_script)
# test_bip322 exercises the BIP-322 primitives of src/common/bip322.c (message hash, to_spend
- # serialization and txid) against the vectors of the BIP.
+ # serialization and txid) against the vectors of the BIP (specs/bip322/, rendered into
+ # bip322_vectors.inc.c by gen.py).
add_executable(test_bip322 test_bip322.c ../src/common/bip322.c)
app_apply_real_sdk_config(test_bip322)
target_link_libraries(test_bip322 PRIVATE cmocka app_crypto write)
### unit-tests/bip322_vectors.inc.c
@@ -0,0 +1,107 @@
+// Generated by specs/bip322/gen.py. DO NOT EDIT.
+// Source: specs/bip322/basic-test-vectors.json (the "tx_hashes" table), a pinned
+// copy of bip-0322/basic-test-vectors.json from the bips repository.
+// clang-format off
+
+typedef struct {
+ const char *message_str; // the message, as a C string literal (for test output)
+ const uint8_t *message; // the UTF-8 encoded message
+ size_t message_len;
+ const char *address;
+ const uint8_t *challenge_script; // scriptPubKey of address
+ size_t challenge_script_len;
+ uint8_t message_hash[32]; // BIP0322-signed-message tagged hash of message
+ uint8_t to_spend_txid[32]; // txid of to_spend, in internal byte order
+ uint8_t to_sign_txid[32]; // txid of to_sign, in internal byte order
+} bip322_tx_hashes_vector_t;
+
+static const uint8_t vec_000_message[] = {
+ 0
+};
+static const uint8_t vec_000_script[] = {
+ 0x00, 0x14, 0x2b, 0x05, 0xd5, 0x64, 0xe6, 0xa7, 0xa3, 0x3c, 0x08, 0x7f, 0x16, 0xe0, 0xf7, 0x30,
+ 0xd1, 0x44, 0x01, 0x23, 0x79, 0x9d,
+};
+
+static const uint8_t vec_001_message[] = {
+ 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64,
+};
+static const uint8_t vec_001_script[] = {
+ 0x00, 0x14, 0x2b, 0x05, 0xd5, 0x64, 0xe6, 0xa7, 0xa3, 0x3c, 0x08, 0x7f, 0x16, 0xe0, 0xf7, 0x30,
+ 0xd1, 0x44, 0x01, 0x23, 0x79, 0x9d,
+};
+
+static const uint8_t vec_002_message[] = {
+ 0x55, 0x54, 0x46, 0x2d, 0x38, 0x20, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x3a, 0x20, 0xc3,
+ 0xb6, 0xc3, 0xa4, 0xc3, 0xbc, 0xc3, 0xa9, 0xc3, 0xa0, 0xc3, 0xa8, 0x20, 0xe6, 0xb5, 0x8b, 0xe8,
+ 0xaf, 0x95, 0xe6, 0x96, 0x87, 0xe6, 0x9c, 0xac, 0x20, 0xf0, 0x9f, 0x98, 0x84,
+};
+static const uint8_t vec_002_script[] = {
+ 0x00, 0x14, 0x2b, 0x05, 0xd5, 0x64, 0xe6, 0xa7, 0xa3, 0x3c, 0x08, 0x7f, 0x16, 0xe0, 0xf7, 0x30,
+ 0xd1, 0x44, 0x01, 0x23, 0x79, 0x9d,
+};
+
+static const bip322_tx_hashes_vector_t BIP322_TX_HASHES_VECTORS[] = {
+ {
+ .message_str = "",
+ .message = vec_000_message,
+ .message_len = 0,
+ .address = "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
+ .challenge_script = vec_000_script,
+ .challenge_script_len = 22,
+ .message_hash = {
+ 0xc9, 0x0c, 0x26, 0x9c, 0x4f, 0x8f, 0xcb, 0xe6, 0x88, 0x0f, 0x72, 0xa7, 0x21, 0xdd, 0xfb, 0xf1,
+ 0x91, 0x42, 0x68, 0xa7, 0x94, 0xcb, 0xb2, 0x1c, 0xfa, 0xfe, 0xe1, 0x37, 0x70, 0xae, 0x19, 0xf1,
+ },
+ .to_spend_txid = {
+ 0xa7, 0x99, 0x5a, 0x54, 0x3c, 0x28, 0x66, 0xb5, 0x1b, 0xa9, 0x65, 0xe7, 0x8d, 0x01, 0xde, 0x5d,
+ 0xb5, 0x04, 0x35, 0xcd, 0xe9, 0xd4, 0x82, 0xbf, 0x60, 0xd8, 0xb8, 0x9b, 0xa6, 0x0a, 0x68, 0xc5,
+ },
+ .to_sign_txid = {
+ 0xd6, 0xae, 0xeb, 0xaa, 0xa3, 0x8b, 0x63, 0xdf, 0x1e, 0xfe, 0xdc, 0xca, 0x1d, 0xe8, 0x27, 0x8a,
+ 0xd7, 0x7f, 0xc6, 0xe6, 0x4d, 0x4c, 0x60, 0xc8, 0x44, 0xba, 0xa5, 0x51, 0xe9, 0x54, 0x96, 0x1e,
+ },
+ },
+ {
+ .message_str = "Hello World",
+ .message = vec_001_message,
+ .message_len = 11,
+ .address = "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
+ .challenge_script = vec_001_script,
+ .challenge_script_len = 22,
+ .message_hash = {
+ 0xf0, 0xeb, 0x03, 0xb1, 0xa7, 0x5a, 0xc6, 0xd9, 0x84, 0x7f, 0x55, 0xc6, 0x24, 0xa9, 0x91, 0x69,
+ 0xb5, 0xdc, 0xcb, 0xa2, 0xa3, 0x1f, 0x5b, 0x23, 0xbe, 0xa7, 0x7b, 0xa2, 0x70, 0xde, 0x0a, 0x7a,
+ },
+ .to_spend_txid = {
+ 0x2b, 0x35, 0x03, 0xd6, 0xa2, 0x61, 0x4d, 0xea, 0xf1, 0x71, 0x6c, 0x23, 0x32, 0x5c, 0x53, 0xe0,
+ 0x51, 0x4b, 0x4a, 0xfc, 0x98, 0x10, 0x1c, 0x77, 0x17, 0x52, 0xad, 0x40, 0x67, 0x19, 0x9d, 0xb7,
+ },
+ .to_sign_txid = {
+ 0xdf, 0xbd, 0x93, 0x9d, 0x8c, 0x5c, 0x9c, 0x14, 0x8c, 0x98, 0x11, 0xa5, 0xaf, 0x56, 0x8d, 0xcb,
+ 0xa1, 0xe9, 0x3a, 0x15, 0x4b, 0xcc, 0x93, 0x5f, 0x14, 0x77, 0x20, 0x6f, 0xe8, 0x7a, 0x73, 0x88,
+ },
+ },
+ {
+ .message_str = "UTF-8 support: \303\266\303\244\303\274\303\251\303\240\303\250 \346\265\213\350\257\225\346\226\207\346\234\254 \360\237\230\204",
+ .message = vec_002_message,
+ .message_len = 45,
+ .address = "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
+ .challenge_script = vec_002_script,
+ .challenge_script_len = 22,
+ .message_hash = {
+ 0x43, 0x93, 0x6b, 0x23, 0x7e, 0xa3, 0x8c, 0x77, 0x94, 0xeb, 0x5d, 0x75, 0x5e, 0x0d, 0x22, 0x0b,
+ 0x6d, 0xb9, 0x2e, 0xbf, 0xc5, 0xc8, 0xf4, 0x82, 0x75, 0x9d, 0x22, 0xb1, 0x28, 0x63, 0x76, 0xd7,
+ },
+ .to_spend_txid = {
+ 0xf0, 0x62, 0xfb, 0xd1, 0xad, 0x12, 0xb7, 0x76, 0xe6, 0x1b, 0x8a, 0x5e, 0x42, 0x98, 0x9c, 0x07,
+ 0x6f, 0x09, 0xd2, 0x5b, 0x17, 0x44, 0x9b, 0xc0, 0x1b, 0xfb, 0x8a, 0xfe, 0x25, 0xf5, 0xf4, 0xc8,
+ },
+ .to_sign_txid = {
+ 0x86, 0x65, 0xbe, 0xc1, 0xf1, 0x58, 0x96, 0xeb, 0x1e, 0xaa, 0x17, 0xd6, 0x1f, 0x81, 0xd1, 0xf1,
+ 0xf7, 0xfa, 0xea, 0x09, 0x81, 0x52, 0xec, 0x19, 0xd0, 0xaf, 0x9e, 0xb8, 0x06, 0x8e, 0x48, 0x8f,
+ },
+ },
+};
+
+#define BIP322_TX_HASHES_VECTORS_COUNT (sizeof(BIP322_TX_HASHES_VECTORS) / sizeof(BIP322_TX_HASHES_VECTORS[0]))
### unit-tests/test_bip322.c
@@ -14,83 +14,193 @@
// Test vectors
// ---------------------------------------------------------------------------
-// scriptPubKey of bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l, the address used by the
-// test vectors of BIP-322
-static const uint8_t CHALLENGE_P2WPKH[] = {0x00, 0x14, 0x2b, 0x05, 0xd5, 0x64, 0xe6, 0xa7,
- 0xa3, 0x3c, 0x08, 0x7f, 0x16, 0xe0, 0xf7, 0x30,
- 0xd1, 0x44, 0x01, 0x23, 0x79, 0x9d};
-
-// BIP0322-signed-message tagged hash of ""
-static const uint8_t MSG_HASH_EMPTY[32] = {
- 0xc9, 0x0c, 0x26, 0x9c, 0x4f, 0x8f, 0xcb, 0xe6, 0x88, 0x0f, 0x72, 0xa7, 0x21, 0xdd, 0xfb, 0xf1,
- 0x91, 0x42, 0x68, 0xa7, 0x94, 0xcb, 0xb2, 0x1c, 0xfa, 0xfe, 0xe1, 0x37, 0x70, 0xae, 0x19, 0xf1};
-
-// BIP0322-signed-message tagged hash of "Hello World"
-static const uint8_t MSG_HASH_HELLO_WORLD[32] = {
- 0xf0, 0xeb, 0x03, 0xb1, 0xa7, 0x5a, 0xc6, 0xd9, 0x84, 0x7f, 0x55, 0xc6, 0x24, 0xa9, 0x91, 0x69,
- 0xb5, 0xdc, 0xcb, 0xa2, 0xa3, 0x1f, 0x5b, 0x23, 0xbe, 0xa7, 0x7b, 0xa2, 0x70, 0xde, 0x0a, 0x7a};
-
-// Expected serialization of the to_spend transaction for "Hello World" and CHALLENGE_P2WPKH.
-// The to_spend transactions (and their txids below) for "" and "Hello World" are given in the
-// test vectors of BIP-322.
+// The "tx_hashes" table of bip-0322/basic-test-vectors.json (pinned copy in specs/bip322/),
+// as generated by specs/bip322/gen.py.
+#include "bip322_vectors.inc.c"
+
+// The one hand-written vector: the full serialization of to_spend for "Hello World", spelled
+// out field by field. The vectors above only pin txids, which would not tell *which* field is
+// wrong if the layout ever regressed; this one does. Its inputs are taken from the vectors.
static const uint8_t TO_SPEND_HELLO_WORLD[] = {
// nVersion = 0
- 0x00, 0x00, 0x00, 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
// 1 input: null prevout hash, index 0xffffffff
- 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0xff, 0xff, 0xff, 0xff,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0xff,
+ 0xff,
+ 0xff,
+ 0xff,
// scriptSig: OP_0 PUSH32(message_hash)
- 0x22, 0x00, 0x20, 0xf0, 0xeb, 0x03, 0xb1, 0xa7, 0x5a, 0xc6, 0xd9, 0x84, 0x7f, 0x55, 0xc6, 0x24,
- 0xa9, 0x91, 0x69, 0xb5, 0xdc, 0xcb, 0xa2, 0xa3, 0x1f, 0x5b, 0x23, 0xbe, 0xa7, 0x7b, 0xa2, 0x70,
- 0xde, 0x0a, 0x7a,
+ 0x22,
+ 0x00,
+ 0x20,
+ 0xf0,
+ 0xeb,
+ 0x03,
+ 0xb1,
+ 0xa7,
+ 0x5a,
+ 0xc6,
+ 0xd9,
+ 0x84,
+ 0x7f,
+ 0x55,
+ 0xc6,
+ 0x24,
+ 0xa9,
+ 0x91,
+ 0x69,
+ 0xb5,
+ 0xdc,
+ 0xcb,
+ 0xa2,
+ 0xa3,
+ 0x1f,
+ 0x5b,
+ 0x23,
+ 0xbe,
+ 0xa7,
+ 0x7b,
+ 0xa2,
+ 0x70,
+ 0xde,
+ 0x0a,
+ 0x7a,
// nSequence = 0
- 0x00, 0x00, 0x00, 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
// 1 output: value 0, the challenge scriptPubKey
- 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x14, 0x2b, 0x05, 0xd5, 0x64,
- 0xe6, 0xa7, 0xa3, 0x3c, 0x08, 0x7f, 0x16, 0xe0, 0xf7, 0x30, 0xd1, 0x44, 0x01, 0x23, 0x79, 0x9d,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x16,
+ 0x00,
+ 0x14,
+ 0x2b,
+ 0x05,
+ 0xd5,
+ 0x64,
+ 0xe6,
+ 0xa7,
+ 0xa3,
+ 0x3c,
+ 0x08,
+ 0x7f,
+ 0x16,
+ 0xe0,
+ 0xf7,
+ 0x30,
+ 0xd1,
+ 0x44,
+ 0x01,
+ 0x23,
+ 0x79,
+ 0x9d,
// nLockTime = 0
- 0x00, 0x00, 0x00, 0x00};
-
-// txid of to_spend for "" (in the byte order used inside transactions, i.e. the reverse of the
-// displayed c5680aa69bb8d860bf82d4e9cd3504b55dde018de765a91bb566283c545a99a7)
-static const uint8_t TO_SPEND_TXID_EMPTY[32] = {
- 0xa7, 0x99, 0x5a, 0x54, 0x3c, 0x28, 0x66, 0xb5, 0x1b, 0xa9, 0x65, 0xe7, 0x8d, 0x01, 0xde, 0x5d,
- 0xb5, 0x04, 0x35, 0xcd, 0xe9, 0xd4, 0x82, 0xbf, 0x60, 0xd8, 0xb8, 0x9b, 0xa6, 0x0a, 0x68, 0xc5};
-
-// txid of to_spend for "Hello World" (reverse of the displayed
-// b79d196740ad5217771c1098fc4a4b51e0535c32236c71f1ea4d61a2d603352b)
-static const uint8_t TO_SPEND_TXID_HELLO_WORLD[32] = {
- 0x2b, 0x35, 0x03, 0xd6, 0xa2, 0x61, 0x4d, 0xea, 0xf1, 0x71, 0x6c, 0x23, 0x32, 0x5c, 0x53, 0xe0,
- 0x51, 0x4b, 0x4a, 0xfc, 0x98, 0x10, 0x1c, 0x77, 0x17, 0x52, 0xad, 0x40, 0x67, 0x19, 0x9d, 0xb7};
-
-// Computes the BIP-322 message hash of a C string.
-static void message_hash(const char *message, uint8_t out[static 32]) {
- cx_sha256_t hash_context;
- bip322_message_hash_init(&hash_context);
- crypto_hash_update(&hash_context.header, message, strlen(message));
- crypto_hash_digest(&hash_context.header, out, 32);
-}
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00};
-static void test_bip322_message_hash(void **state) {
- (void) state;
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
- uint8_t hash[32];
+static const bip322_tx_hashes_vector_t *find_vector(const char *message_str) {
+ for (size_t i = 0; i < BIP322_TX_HASHES_VECTORS_COUNT; i++) {
+ if (strcmp(BIP322_TX_HASHES_VECTORS[i].message_str, message_str) == 0) {
+ return &BIP322_TX_HASHES_VECTORS[i];
+ }
+ }
+ return NULL;
+}
- message_hash("", hash);
- assert_memory_equal(hash, MSG_HASH_EMPTY, 32);
+static void sha256d(const uint8_t *data, size_t len, uint8_t out[32]) {
+ cx_hash_sha256(data, len, out, 32);
+ cx_hash_sha256(out, 32, out, 32);
+}
- message_hash("Hello World", hash);
- assert_memory_equal(hash, MSG_HASH_HELLO_WORLD, 32);
+// Serializes the BIP-322 to_sign transaction spending to_spend_txid:0 (test-side
+// reimplementation; the app never builds to_sign itself, the client does).
+static size_t serialize_to_sign(const uint8_t to_spend_txid[32], uint8_t out[128]) {
+ size_t off = 0;
+ memset(out + off, 0, 4); // nVersion = 0
+ off += 4;
+ out[off++] = 0x01; // 1 input
+ memcpy(out + off, to_spend_txid, 32);
+ off += 32;
+ memset(out + off, 0, 4); // prevout index 0
+ off += 4;
+ out[off++] = 0x00; // empty scriptSig
+ memset(out + off, 0, 4); // nSequence = 0
+ off += 4;
+ out[off++] = 0x01; // 1 output
+ memset(out + off, 0, 8); // value 0
+ off += 8;
+ out[off++] = 0x01; // OP_RETURN script
+ out[off++] = 0x6a;
+ memset(out + off, 0, 4); // nLockTime = 0
+ off += 4;
+ return off;
}
-static void test_bip322_serialize_to_spend(void **state) {
+// ---------------------------------------------------------------------------
+// Tests: to_spend construction
+// ---------------------------------------------------------------------------
+
+static void test_bip322_serialize_to_spend_layout(void **state) {
(void) state;
+ const bip322_tx_hashes_vector_t *v = find_vector("Hello World");
+ assert_non_null(v);
+
uint8_t out[BIP322_TO_SPEND_MAX_LEN];
- int len = bip322_serialize_to_spend(MSG_HASH_HELLO_WORLD,
- CHALLENGE_P2WPKH,
- sizeof(CHALLENGE_P2WPKH),
+ int len = bip322_serialize_to_spend(v->message_hash,
+ v->challenge_script,
+ v->challenge_script_len,
out);
assert_int_equal(len, sizeof(TO_SPEND_HELLO_WORLD));
@@ -102,37 +212,95 @@ static void test_bip322_serialize_to_spend_rejects_long_script(void **state) {
uint8_t long_script[MAX_PREVOUT_SCRIPTPUBKEY_LEN + 1] = {0};
uint8_t out[BIP322_TO_SPEND_MAX_LEN];
- assert_int_equal(
- bip322_serialize_to_spend(MSG_HASH_HELLO_WORLD, long_script, sizeof(long_script), out),
- -1);
+ assert_int_equal(bip322_serialize_to_spend(BIP322_TX_HASHES_VECTORS[0].message_hash,
+ long_script,
+ sizeof(long_script),
+ out),
+ -1);
}
-static void test_bip322_compute_to_spend_txid(void **state) {
+// For every vector: the serialized to_spend hashes to the vector's to_spend txid, and so does
+// the txid computed by bip322_compute_to_spend_txid().
+static void test_bip322_vectors_to_spend_txid(void **state) {
(void) state;
- uint8_t txid[32];
-
- assert_int_equal(bip322_compute_to_spend_txid(MSG_HASH_EMPTY,
- CHALLENGE_P2WPKH,
- sizeof(CHALLENGE_P2WPKH),
- txid),
- 0);
- assert_memory_equal(txid, TO_SPEND_TXID_EMPTY, 32);
-
- assert_int_equal(bip322_compute_to_spend_txid(MSG_HASH_HELLO_WORLD,
- CHALLENGE_P2WPKH,
- sizeof(CHALLENGE_P2WPKH),
- txid),
- 0);
- assert_memory_equal(txid, TO_SPEND_TXID_HELLO_WORLD, 32);
+ for (size_t i = 0; i < BIP322_TX_HASHES_VECTORS_COUNT; i++) {
+ const bip322_tx_hashes_vector_t *v = &BIP322_TX_HASHES_VECTORS[i];
+ print_message("vector %zu: %s\n", i, v->message_str);
+
+ uint8_t to_spend[BIP322_TO_SPEND_MAX_LEN];
+ int len = bip322_serialize_to_spend(v->message_hash,
+ v->challenge_script,
+ v->challenge_script_len,
+ to_spend);
+ assert_true(len > 0);
+
+ uint8_t txid[32];
+ sha256d(to_spend, (size_t) len, txid);
+ assert_memory_equal(txid, v->to_spend_txid, 32);
+
+ memset(txid, 0, sizeof(txid));
+ assert_int_equal(bip322_compute_to_spend_txid(v->message_hash,
+ v->challenge_script,
+ v->challenge_script_len,
+ txid),
+ 0);
+ assert_memory_equal(txid, v->to_spend_txid, 32);
+ }
+}
+
+// For every vector: the to_sign transaction built on the txid the app computes hashes to the
+// vector's to_sign txid. This is what the app's signature ultimately commits to.
+static void test_bip322_vectors_to_sign_txid(void **state) {
+ (void) state;
+
+ for (size_t i = 0; i < BIP322_TX_HASHES_VECTORS_COUNT; i++) {
+ const bip322_tx_hashes_vector_t *v = &BIP322_TX_HASHES_VECTORS[i];
+
+ uint8_t to_spend_txid[32];
+ assert_int_equal(bip322_compute_to_spend_txid(v->message_hash,
+ v->challenge_script,
+ v->challenge_script_len,
+ to_spend_txid),
+ 0);
+
+ uint8_t to_sign[128];
+ size_t len = serialize_to_sign(to_spend_txid, to_sign);
+
+ uint8_t txid[32];
+ sha256d(to_sign, len, txid);
+ assert_memory_equal(txid, v->to_sign_txid, 32);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Tests: message hash
+// ---------------------------------------------------------------------------
+
+// For every vector: the message hashes to the vector's message_hash.
+static void test_bip322_vectors_message_hash(void **state) {
+ (void) state;
+
+ for (size_t i = 0; i < BIP322_TX_HASHES_VECTORS_COUNT; i++) {
+ const bip322_tx_hashes_vector_t *v = &BIP322_TX_HASHES_VECTORS[i];
+ print_message("vector %zu: %s\n", i, v->message_str);
+
+ cx_sha256_t hash_context;
+ bip322_message_hash_init(&hash_context);
+ crypto_hash_update(&hash_context.header, v->message, v->message_len);
+ uint8_t hash[32];
+ crypto_hash_digest(&hash_context.header, hash, 32);
+ assert_memory_equal(hash, v->message_hash, 32);
+ }
}
int main(void) {
const struct CMUnitTest tests[] = {
- cmocka_unit_test(test_bip322_message_hash),
- cmocka_unit_test(test_bip322_serialize_to_spend),
+ cmocka_unit_test(test_bip322_serialize_to_spend_layout),
cmocka_unit_test(test_bip322_serialize_to_spend_rejects_long_script),
- cmocka_unit_test(test_bip322_compute_to_spend_txid),
+ cmocka_unit_test(test_bip322_vectors_to_spend_txid),
+ cmocka_unit_test(test_bip322_vectors_to_sign_txid),
+ cmocka_unit_test(test_bip322_vectors_message_hash),
};
return cmocka_run_group_tests(tests, NULL, NULL);Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.