Last active
May 29, 2026 00:22
-
-
Save qlrd/d61228c1b33d2e93194734233df32ecd to your computer and use it in GitHub Desktop.
krux BIP-137 — compare pre-fix vs post-fix signing for P2PKH / P2SH-P2WPKH / P2WPKH
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """bip137_old_vs_new.py — pre-fix vs post-fix krux BIP-137 message signing. | |
| Reproduces the two krux signing variants side-by-side: | |
| - "old": the pre-fix behavior. `key.sign_at` emits a header byte in the | |
| P2PKH-compressed range (31-34) regardless of the wallet's actual | |
| script type, so signatures for P2WPKH / P2SH-P2WPKH addresses | |
| are non-compliant with BIP-137 §"Header byte". | |
| - "new": the post-fix behavior. The header byte is rewritten per the | |
| actual script type — 31-34 for P2PKH, 35-38 for P2SH-P2WPKH, | |
| 39-42 for P2WPKH — so a strict verifier reconstructing the | |
| address from the recovered pubkey lands on the right script. | |
| For non-P2PKH addresses the two outputs differ only in the first byte | |
| (the header). The 64-byte ECDSA portion is identical because the same | |
| hash was signed with the same key. | |
| Run: | |
| pip install embit | |
| python bip137_old_vs_new.py | |
| Verify live in Sparrow → Tools → Sign/Verify Message (BIP-322 toggle OFF): | |
| paste the printed address + signature for each case | |
| `old` is expected to fail strict (raw) verification, `new` to pass. | |
| Test mnemonic is the well-known BIP-39 vector — do not use real keys. | |
| """ | |
| import base64 | |
| import hashlib | |
| from embit import bip32, bip39, compact, ec, script | |
| from embit.networks import NETWORKS | |
| from embit.util import secp256k1 | |
| SCRIPT_TYPE_HEADER_BASE = { | |
| "p2pkh": 31, # 31-34, compressed | |
| "p2sh-p2wpkh": 35, # 35-38 | |
| "p2wpkh": 39, # 39-42 | |
| } | |
| def message_commitment(message: bytes) -> bytes: | |
| """BIP-137 double-SHA256 commitment over the magic-prefixed payload.""" | |
| return hashlib.sha256( | |
| hashlib.sha256( | |
| b"\x18Bitcoin Signed Message:\n" | |
| + compact.to_bytes(len(message)) | |
| + message | |
| ).digest() | |
| ).digest() | |
| def _sign_recoverable(privkey: bytes, msg_hash: bytes) -> bytes: | |
| """Mirrors `krux.key.Key.sign_at`: always emits a P2PKH-compressed header. | |
| Returns 65 bytes: header (1) || compact_ecdsa (64). | |
| """ | |
| sig = secp256k1.ecdsa_sign_recoverable(msg_hash, privkey) | |
| flag = bytes([27 + sig[64] + 4]) | |
| ec_signature = ec.Signature(sig[:64]) | |
| return flag + secp256k1.ecdsa_signature_serialize_compact(ec_signature._sig) | |
| def sign_old(message: bytes, root, derivation_str: str) -> bytes: | |
| """Pre-fix krux: same logic as the old inline `_sign_at_address`.""" | |
| path = bip32.parse_path(derivation_str) | |
| # access the private scalar bytes directly, same as key.sign_at does | |
| privkey_bytes = root.derive(path).key._secret | |
| return _sign_recoverable(privkey_bytes, message_commitment(message)) | |
| def sign_new(message: bytes, root, derivation_str: str, script_type: str) -> bytes: | |
| """Post-fix krux: rewrites the header byte for the actual script type.""" | |
| raw = sign_old(message, root, derivation_str) | |
| recovery_id = raw[0] - 31 | |
| if not 0 <= recovery_id <= 3: | |
| raise ValueError("unexpected raw header: %d" % raw[0]) | |
| base = SCRIPT_TYPE_HEADER_BASE.get(script_type) | |
| if base is None: | |
| raise ValueError("unsupported script_type: %s" % script_type) | |
| return bytes([base + recovery_id]) + raw[1:] | |
| def derive_address(root, derivation_str: str, script_type: str, network: str) -> str: | |
| path = bip32.parse_path(derivation_str) | |
| pub = root.derive(path).to_public().key | |
| net = NETWORKS[network] | |
| if script_type == "p2pkh": | |
| return script.p2pkh(pub).address(network=net) | |
| if script_type == "p2sh-p2wpkh": | |
| return script.p2sh(script.p2wpkh(pub)).address(network=net) | |
| if script_type == "p2wpkh": | |
| return script.p2wpkh(pub).address(network=net) | |
| raise ValueError(script_type) | |
| SCRIPT_TYPES = [ | |
| # (script_type, BIP-44 purpose) | |
| ("p2pkh", 44), | |
| ("p2sh-p2wpkh", 49), | |
| ("p2wpkh", 84), | |
| ] | |
| # Network → SLIP-44 coin type. All test variants use coin_type 1. | |
| # (Mainnet intentionally excluded — never sign real-wallet test vectors.) | |
| NETWORK_COIN_TYPE = {"test": 1, "regtest": 1, "signet": 1} | |
| def main(): | |
| MNEMONIC = ( | |
| "abandon abandon abandon abandon abandon abandon " | |
| "abandon abandon abandon abandon abandon about" | |
| ) | |
| MESSAGE = b"a test message." | |
| seed = bip39.mnemonic_to_seed(MNEMONIC) | |
| root = bip32.HDKey.from_seed(seed) | |
| print("mnemonic: %s" % MNEMONIC) | |
| print("message: %s" % MESSAGE.decode()) | |
| print() | |
| for script_type, purpose in SCRIPT_TYPES: | |
| for network, coin_type in NETWORK_COIN_TYPE.items(): | |
| deriv = "m/%dh/%dh/0h/0/3" % (purpose, coin_type) | |
| addr = derive_address(root, deriv, script_type, network) | |
| old = sign_old(MESSAGE, root, deriv) | |
| new = sign_new(MESSAGE, root, deriv, script_type) | |
| print("--- %s (%s, %s) ---" % (script_type, deriv, network)) | |
| print("address: %s" % addr) | |
| print( | |
| "old header: 0x%02x (%d, P2PKH-compressed range)" | |
| % (old[0], old[0]) | |
| ) | |
| print("old b64: %s" % base64.b64encode(old).decode()) | |
| print( | |
| "new header: 0x%02x (%d, %s range)" | |
| % (new[0], new[0], script_type) | |
| ) | |
| print("new b64: %s" % base64.b64encode(new).decode()) | |
| print("ECDSA bytes identical: %s" % (old[1:] == new[1:])) | |
| print() | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment