Repository navigation
fix(scrolls): bind script signatures to the creating transaction's first input - #269
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Normal prover builds still embed unchanged guest ELFs and keys, preserving the replay vulnerability and rejecting the canister’s new transaction-bound signatures.
1 open issue
What changed in this PR
This PR binds Bitcoin Scrolls script signatures to the creating transaction’s first input, preventing signed script maps from being reused across transactions.
Changes:
- Shares transaction-bound CBOR serialization between the canister signer and client verifier.
- Adds regression tests for valid, unbound, and foreign-input signatures.
- Updates signature documentation and uploads rebuilt guest artifacts when CI fails.
| File | Description |
|---|---|
src/tx/scrolls_bitcoin.rs |
Updates the signature-format documentation. |
scrolls/src/scrolls_bitcoin/src/lib.rs |
Signs script maps together with the first input. |
scrolls/src/scrolls_bitcoin/scrolls_bitcoin.did |
Updates comments describing signed data; interface unchanged. |
charms-data/src/lib.rs |
Adds shared signature-preimage serialization and a binding test. |
charms-client/src/lib.rs |
Verifies transaction-bound signatures and adds regression tests. |
.github/workflows/guests.yml |
Uploads rebuilt guests and key files after CI failure. |
Review effort: Balanced
| s: &SignedScrollOutputs, | ||
| pubkey: &[u8; 32], | ||
| ) -> anyhow::Result<()> { | ||
| let message_bytes = signed_scroll_script_pubkeys_preimage(tx_in_0, &s.script_pubkeys) |
Contributor
[High impact] The PR appears safe to merge; no blocking findings remain. SummaryScrolls signatures now cover the creating transaction’s first input and its script map.
Reviews (7) · Last reviewed commit: "fix: record the v16 guest that rejects c..." · Reviewed by Greptile |
cursor
Bot
force-pushed
the
cursor/bind-scroll-first-input-0e39
branch
from
November 3, 2026 01:51
9f75ad2 to
014cb01
Compare
check_scroll_outputs accepts a Schnorr signature over the scriptPubKey map alone, so that signature verifies for any spell that copies those scripts. A signature that also covers the spell's first input does not verify. Co-authored-by: Ivan Mikushin <imikushin@users.noreply.github.com>
…st input sign_script_pubkeys and check_scroll_outputs now sign and verify SHA-256 of CBOR (first input outpoint, scriptPubKey map). A map signed for one creating transaction no longer verifies for another. Co-authored-by: Ivan Mikushin <imikushin@users.noreply.github.com>
The guest check rebuilds the ELFs in the worktree and then exits. The upload keeps those bytes so a later commit can record them. Co-authored-by: Ivan Mikushin <imikushin@users.noreply.github.com>
The candid addresses note still said the signature covered only the script map. Co-authored-by: Ivan Mikushin <imikushin@users.noreply.github.com>
…input The guest rebuild of this source produced program key 0x00c64799fb912b8ac4ad558ab7c6393f478e53a4a0b80d3721265ec615d1484b. The Solidity program key and the vk package tests use that key. The temporary guest artifact upload is removed. Co-authored-by: Ivan Mikushin <imikushin@users.noreply.github.com>
The stored Bitcoin spell has no inputs. The guest checks the spell before those inputs are stripped, and verification fills them back from the hosting transaction. A coinbase creator still uses a different outpoint on the spend path. Co-authored-by: Ivan Mikushin <imikushin@users.noreply.github.com>
The checked-in guest is the Litecoin build. The Scrolls binding changes the guest again, so the next check should keep the rebuilt bytes. Co-authored-by: Ivan Mikushin <imikushin@users.noreply.github.com>
The rebuilt program key is 0x004be4c872e085a4cc485cd9db9193fa1e534fef6b4ec09897e863a168efd7a6. Version 16 still has that one key. The temporary guest upload is removed. Co-authored-by: Ivan Mikushin <imikushin@users.noreply.github.com>
The checked-in guest is the beaming build. The Scrolls binding changes the guest again, so the next check should keep the rebuilt bytes. The note now follows host_scrolls. Co-authored-by: Ivan Mikushin <imikushin@users.noreply.github.com>
cursor
Bot
force-pushed
the
cursor/bind-scroll-first-input-0e39
branch
from
November 3, 2026 04:33
5123d01 to
e8dd13b
Compare
The rebuilt program key is 0x00c57bcd3119afe7b5922c255e5f84a94839f4ccd462caa2a732eb5ba8745daa. Version 16 still has that one key. The temporary guest upload is removed. Co-authored-by: Ivan Mikushin <imikushin@users.noreply.github.com>
The filled first input of a coinbase is the null outpoint, and every coinbase shares it. The spell check, the prove fill, and addresses refuse that anchor. sign_tx still spends an existing coinbase output with the block height. Co-authored-by: Ivan Mikushin <imikushin@users.noreply.github.com>
The spell check changed the guest. The next failed rebuild should keep the new bytes. Co-authored-by: Ivan Mikushin <imikushin@users.noreply.github.com>
The rebuilt program key is 0x00990da83b7a7a3e54273d6e5ec48763d6b7fa4228fbb0552e109ace394ead5e. Version 16 still has that one key. The temporary guest upload is removed. Co-authored-by: Ivan Mikushin <imikushin@users.noreply.github.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Why
A Scrolls signature covers only the output index and the script.
check_scroll_outputsdoes not bind that signature to the spell transaction's first input, so a signed map can be reused on another transaction. A script Scrolls will never withdraw from can still count as a Scrolls output. This change puts that first input into the bytes Scrolls signs and the bytes the spell check verifies.A coinbase spell's first input is the null outpoint. Every coinbase shares it, so one signed map would verify on every coinbase spell, and
sign_txwould still derive the withdrawal key from the zero txid and the block height. Those outputs could not be withdrawn. Coinbase transactions cannot create Scrolls outputs.What changed
The signed statement is CBOR of the creating transaction's first input and the script map. The canister and
check_scroll_outputsboth hash those bytes. A reused or foreign map is rejected. A map bound to this spell's first input passes.check_scroll_outputsrejects a spell that declares Scrolls outputs when the first input filled from the hosting transaction is the null outpoint (all-zero txid, vout0xffffffff). A coinbase spell with no Scrolls outputs still verifies.fill_scroll_outputsreturns that error before it calls the canister.addressesrefuses the null outpoint.sign_tx's existing coinbase spend path is unchanged.The branch includes the Litecoin beaming work.
host_scrollsstill runs the signature check only for a Bitcoin host, and a Litecoin host that lists scroll outputs is rejected. The guest is rebuilt for that layout, this check, and the coinbase ban. Spell v16 has one program key,0x00990da83b7a7a3e54273d6e5ec48763d6b7fa4228fbb0552e109ace394ead5e. There is no fallback key. The Groth16 verifying key stays the v15 key. The SolidityPROGRAM_VKEYmatches this program key.The stored Bitcoin spell has no inputs. The guest checks the spell before those inputs are stripped, and verification fills them back from the hosting transaction. That is why the check binds the hosting transaction, and why the coinbase ban reads that filled input. The note is
docs/solutions/security-issues/scrolls-script-map-signature-unbound-from-first-input.md.Scope
Bitcoin Scrolls output creation and the spell check that accepts those outputs. No app-specific case. No canister deploy.
Tradeoffs
The signed object does not grow a separate outpoint field. The spell already carries the first input, and proof verification fills that input from the hosting transaction before it checks the proof. A second copy would need its own equality check.
Canisters
scrolls_bitcoin_v15(rpgc6-oqaaa-aaaak-qy3uq-cai) needs this wasm before a new client will accept its signatures. Do not upgradescrolls_bitcoin_v14(lmbwh-3qaaa-aaaak-qunha-cai). It still serves the v14signandaddressmethods.scrolls_bitcoin_v16has no id incanister_ids.json.Verification
Before the fix,
check_scroll_outputs_accepts_map_bound_to_the_spells_first_inputfailed withsignature failed verification.check_scroll_outputs_rejects_a_reused_or_foreign_signed_mapfailed because an unbound signature returnedOk(true).After the fix, those Scrolls tests passed, along with
litecoin_host_rejects_listed_scroll_outputs,coinbase_spell_with_scroll_outputs_is_rejected, andcoinbase_spell_without_scroll_outputs_still_passes_the_scroll_check.fill_scroll_outputsrejects a coinbase before calling the canister and allows a coinbase with no Scrolls outputs.addresses_rejects_the_null_coinbase_outpointpasses, and a block-height anchor is not treated as the null outpoint.cargo test --locked --manifest-path charms-proof-wrapper/vk/Cargo.tomlpassed 5 tests, including the Solidity program-key check.cached_sp1_vks_match_embeddedandprograms_run_in_zkvmpassed on the rebuilt guest.The committed guest bytes are the guest workflow rebuild of this source (
145a220). Their git hashes match that run's worktree.