Skip to content

fix(scrolls): bind script signatures to the creating transaction's first input - #269

Merged
imikushin merged 13 commits into
mainfrom
cursor/bind-scroll-first-input-0e39
Nov 3, 2026
Merged

imikushin merged 13 commits into
mainfrom
cursor/bind-scroll-first-input-0e39

Conversation

@imikushin

@imikushin imikushin commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Why

A Scrolls signature covers only the output index and the script. check_scroll_outputs does 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_tx would 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_outputs both hash those bytes. A reused or foreign map is rejected. A map bound to this spell's first input passes.

check_scroll_outputs rejects a spell that declares Scrolls outputs when the first input filled from the hosting transaction is the null outpoint (all-zero txid, vout 0xffffffff). A coinbase spell with no Scrolls outputs still verifies. fill_scroll_outputs returns that error before it calls the canister. addresses refuses the null outpoint. sign_tx's existing coinbase spend path is unchanged.

The branch includes the Litecoin beaming work. host_scrolls still 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 Solidity PROGRAM_VKEY matches 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 upgrade scrolls_bitcoin_v14 (lmbwh-3qaaa-aaaak-qunha-cai). It still serves the v14 sign and address methods. scrolls_bitcoin_v16 has no id in canister_ids.json.

Verification

Before the fix, check_scroll_outputs_accepts_map_bound_to_the_spells_first_input failed with signature failed verification. check_scroll_outputs_rejects_a_reused_or_foreign_signed_map failed because an unbound signature returned Ok(true).

After the fix, those Scrolls tests passed, along with litecoin_host_rejects_listed_scroll_outputs, coinbase_spell_with_scroll_outputs_is_rejected, and coinbase_spell_without_scroll_outputs_still_passes_the_scroll_check. fill_scroll_outputs rejects a coinbase before calling the canister and allows a coinbase with no Scrolls outputs. addresses_rejects_the_null_coinbase_outpoint passes, and a block-height anchor is not treated as the null outpoint. cargo test --locked --manifest-path charms-proof-wrapper/vk/Cargo.toml passed 5 tests, including the Solidity program-key check. cached_sp1_vks_match_embedded and programs_run_in_zkvm passed 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.

Open in Web Open in Cursor 

Copilot AI balanced review requested due to automatic review settings November 3, 2026 00:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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

Comment thread charms-client/src/lib.rs
s: &SignedScrollOutputs,
pubkey: &[u8; 32],
) -> anyhow::Result<()> {
let message_bytes = signed_scroll_script_pubkeys_preimage(tx_in_0, &s.script_pubkeys)
@greptile-apps

greptile-apps Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The PR appears safe to merge; no blocking findings remain.

Summary

Scrolls signatures now cover the creating transaction’s first input and its script map.

  • Scrolls signatures now cover the first input and script map.
  • Coinbase transactions cannot create Scrolls outputs.
  • Version-16 proofs now use the rebuilt spell-checker key.
  • The HTTP status conversion and test fixture keep the same behavior.

Reviews (7) · Last reviewed commit: "fix: record the v16 guest that rejects c..." · Reviewed by Greptile

Comment thread .github/workflows/guests.yml Resolved
@cursor
cursor Bot force-pushed the cursor/bind-scroll-first-input-0e39 branch from 9f75ad2 to 014cb01 Compare November 3, 2026 01:51
cursoragent and others added 9 commits November 3, 2026 04:30
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
cursor Bot force-pushed the cursor/bind-scroll-first-input-0e39 branch from 5123d01 to e8dd13b Compare November 3, 2026 04:33
cursoragent and others added 3 commits November 3, 2026 04:40
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>
Comment thread charms-client/src/lib.rs Resolved
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>
@imikushin
imikushin merged commit c7727bb into main Nov 3, 2026
7 checks passed
@imikushin
imikushin deleted the cursor/bind-scroll-first-input-0e39 branch November 3, 2026 15:31
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.

3 participants