Skip to content

Instantly share code, notes, and snippets.

@qlrd
Last active May 29, 2026 00:22
Show Gist options
  • Select an option

  • Save qlrd/d61228c1b33d2e93194734233df32ecd to your computer and use it in GitHub Desktop.

Select an option

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
#!/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