Skip to content

Add sighash methods - #58

Open
stringhandler wants to merge 2 commits into
BlockstreamResearch:masterfrom
stringhandler:st-sighash
Open

stringhandler wants to merge 2 commits into
BlockstreamResearch:masterfrom
stringhandler:st-sighash

Conversation

@stringhandler

Copy link
Copy Markdown
Collaborator

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.

  • This PR suggests a bug fix and I've added the necessary tests.
  • This PR introduces a new feature and I've discussed the update in an Issue or with the team.
  • This PR is just a minor change like a typo fix.

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.
Comment thread simf/sighash_elements.simf Outdated
Comment on lines +2 to +30
/// 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
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these tags correspond to an existing standard, or are they a new feature?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a new feature. I've added a lot of extra documentation here.

Comment thread simf/sighash_elements.simf Outdated
Comment on lines +98 to +99
/// 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can the current function be equivalent to jet::sig_all_hash if jet::sig_all_hash does not contain the tag_all tag?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread simf/sighash_elements.simf Outdated
Comment on lines +32 to +96
/// 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)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread simf/sighash_elements.simf Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also move the file to the lib directory and add the necessary tests for the new functions?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
@stringhandler

Copy link
Copy Markdown
Collaborator Author

Added a huge amount of documentation and tests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants