Add sighash methods - #58
stringhandler wants to merge 2 commits into
Conversation
Adds simf/sighash_elements.simf, which implements the six sighash modes on Elements: ALL, NONE, SINGLE, and their ANYONECANPAY variants. Each mode uses its own tagged hash, so a signature made in one mode can't be reused in another. Every mode also commits to the genesis block hash, which stops a signature from being replayed on another Elements chain. If there's no output at this input's index, SINGLE panics instead of signing the legacy fixed digest.
| /// sha256("SimplicityHL/sighash/v1/all") | ||
| fn tag_all() -> u256 { | ||
| 0x0e8e05b1734bb78560eec1e2153340c5da8a5d7dc936253996d70aeacc26923a | ||
| } | ||
|
|
||
| /// sha256("SimplicityHL/sighash/v1/none") | ||
| fn tag_none() -> u256 { | ||
| 0x23ec67ac4f3c0762d9f8b5966a067d1f871d3068fb23325022ea1c6545eb52ea | ||
| } | ||
|
|
||
| /// sha256("SimplicityHL/sighash/v1/single") | ||
| fn tag_single() -> u256 { | ||
| 0x1d20b67e40936bfa4472f96707752a4694bff7f699cd2efbc27d10bfa8ae97ed | ||
| } | ||
|
|
||
| /// sha256("SimplicityHL/sighash/v1/all_anyonecanpay") | ||
| fn tag_all_anyonecanpay() -> u256 { | ||
| 0x7b8e176217cf4be9d5ade17ac856ebfad72b2be99fc3199db092b08642bd4333 | ||
| } | ||
|
|
||
| /// sha256("SimplicityHL/sighash/v1/none_anyonecanpay") | ||
| fn tag_none_anyonecanpay() -> u256 { | ||
| 0xde3aea875d6db910ca2ce189cda1d67c7e1ceacde2f36fd95cb52aa17de7f23b | ||
| } | ||
|
|
||
| /// sha256("SimplicityHL/sighash/v1/single_anyonecanpay") | ||
| fn tag_single_anyonecanpay() -> u256 { | ||
| 0xac1b092763ce7c385be49d56642802f8b21ddd81deeca183f894f52879b9a981 | ||
| } |
There was a problem hiding this comment.
Do these tags correspond to an existing standard, or are they a new feature?
There was a problem hiding this comment.
It's a new feature. I've added a lot of extra documentation here.
| /// All inputs, all outputs. The equivalent of `jet::sig_all_hash` — prefer that jet, which is | ||
| /// cheaper; this exists so all six modes read the same way. |
There was a problem hiding this comment.
How can the current function be equivalent to jet::sig_all_hash if jet::sig_all_hash does not contain the tag_all tag?
There was a problem hiding this comment.
Good find. I've changed the sig_all_hash to call the jet directly. This means it differs significantly from the other methods, but makes it easier for wallets to integrate
| /// Start a mode's hash, seeded with its tag twice (BIP-340 tagged-hash style). | ||
| fn mode_ctx(tag: u256) -> Ctx8 { | ||
| let ctx: Ctx8 = jet::sha_256_ctx_8_init(); | ||
| let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, tag); | ||
| jet::sha_256_ctx_8_add_32(ctx, tag) | ||
| } | ||
|
|
||
| /// nVersion and nLockTime, committed to by every mode — as in BIP-341, where both appear in | ||
| /// the message regardless of the flag. | ||
| fn add_common(ctx: Ctx8) -> Ctx8 { | ||
| let ctx: Ctx8 = jet::sha_256_ctx_8_add_4(ctx, jet::version()); | ||
| jet::sha_256_ctx_8_add_4(ctx, jet::tx_lock_time()) | ||
| } | ||
|
|
||
| /// Every input: outpoints, sequences, and the UTXOs they spend (values, assets, scripts). | ||
| /// The non-ANYONECANPAY case — no input may be added, removed or reordered after signing. | ||
| fn add_all_inputs(ctx: Ctx8) -> Ctx8 { | ||
| let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, jet::inputs_hash()); | ||
| jet::sha_256_ctx_8_add_32(ctx, jet::input_utxos_hash()) | ||
| } | ||
|
|
||
| /// This input alone. The ANYONECANPAY case — others may be added freely afterwards, which is | ||
| /// what lets a fee input be attached to a signature made months earlier. | ||
| fn add_current_input(ctx: Ctx8) -> Ctx8 { | ||
| let i: u32 = jet::current_index(); | ||
| let ctx: Ctx8 = match jet::input_hash(i) { | ||
| Some(h: u256) => jet::sha_256_ctx_8_add_32(ctx, h), | ||
| None => panic!(), | ||
| }; | ||
| match jet::input_utxo_hash(i) { | ||
| Some(h: u256) => jet::sha_256_ctx_8_add_32(ctx, h), | ||
| None => panic!(), | ||
| } | ||
| } | ||
|
|
||
| /// Every output. On Elements this covers assets, amounts, nonces and the range and surjection | ||
| /// proofs as well as the scripts. | ||
| fn add_all_outputs(ctx: Ctx8) -> Ctx8 { | ||
| jet::sha_256_ctx_8_add_32(ctx, jet::outputs_hash()) | ||
| } | ||
|
|
||
| /// The output at this input's index — SIGHASH_SINGLE's pairing rule. Aborts when there is no | ||
| /// such output, rather than signing a fixed digest the way legacy Bitcoin does. | ||
| fn add_current_output(ctx: Ctx8) -> Ctx8 { | ||
| match jet::output_hash(jet::current_index()) { | ||
| Some(h: u256) => jet::sha_256_ctx_8_add_32(ctx, h), | ||
| None => panic!(), | ||
| } | ||
| } | ||
|
|
||
| /// Close a mode: bind the transaction digest to this chain, this tapleaf, and this input's | ||
| /// position. Mirrors the outer structure of `jet::sig_all_hash`. | ||
| /// | ||
| /// `genesis_block_hash` is the Elements-only part — it stops a signature made for one Elements | ||
| /// chain from being replayed on another. The Bitcoin edition of this module omits it, because | ||
| /// Bitcoin's own sighash does not commit to a genesis hash either. | ||
| fn finish(ctx: Ctx8) -> u256 { | ||
| let tx: u256 = jet::sha_256_ctx_8_finalize(ctx); | ||
| let outer: Ctx8 = jet::sha_256_ctx_8_init(); | ||
| let outer: Ctx8 = jet::sha_256_ctx_8_add_32(outer, jet::genesis_block_hash()); | ||
| let outer: Ctx8 = jet::sha_256_ctx_8_add_32(outer, tx); | ||
| let outer: Ctx8 = jet::sha_256_ctx_8_add_32(outer, jet::tap_env_hash()); | ||
| let outer: Ctx8 = jet::sha_256_ctx_8_add_4(outer, jet::current_index()); | ||
| jet::sha_256_ctx_8_finalize(outer) | ||
| } |
There was a problem hiding this comment.
These functions help with SHA-256 and should go in a separate file. It’s also best to open a separate pull request to add them to Std
There was a problem hiding this comment.
I don't agree. They are specific to the signatures. They are also not pub, so I don't think its necessary to put them in another file
There was a problem hiding this comment.
Could you also move the file to the lib directory and add the necessary tests for the new functions?
There was a problem hiding this comment.
Moved and added
Add simf/lib/sighash_elements.simf, which lets Simplicity contracts check BIP-340 signatures in the six BIP-341 sighash modes on Elements. - ALL is jet::sig_all_hash. NONE, SINGLE and the ANYONECANPAY variants are tagged hashes bound to the chain, the contract and (except ANYONECANPAY) the input index. - sighash(mode) accepts 0x01-0x03 and 0x81-0x83. Anything else, including 0x00, panics. - bip_0340_verify_with_mode verifies a signature over sighash(mode). Add docs/sighash_elements.md, a byte-level spec for wallet and signer implementers, with test vectors and stdlib.json entries. The code is marked unaudited throughout. Tests check what each mode signs and that an independent implementation of the spec matches the contract and the vectors. Regtest elementsd accepts a spend in every mode. Assisted-by: Claude Opus 5.5 (Claude Code)
d65cea7 to
bcf5947
Compare
|
Added a huge amount of documentation and tests |
Adds simf/sighash_elements.simf, which implements the six sighash modes on Elements: ALL, NONE, SINGLE, and their ANYONECANPAY variants.
Each mode uses its own tagged hash, so a signature made in one mode can't be reused in another. Every mode also commits to the genesis block
hash, which stops a signature from being replayed on another Elements
chain. If there's no output at this input's index, SINGLE panics
instead of signing the legacy fixed digest.