Skip to content

fix(joy-id): return a PSBT from signPsbt, and switch to @scure/btc-signer - #581

Open
fghdotio wants to merge 4 commits into
ckb-devrel:devfrom
fghdotio:refactor/scure-btc-signer
Open

fghdotio wants to merge 4 commits into
ckb-devrel:devfrom
fghdotio:refactor/scure-btc-signer

Conversation

@fghdotio

@fghdotio fghdotio commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Refs #517. Supersedes #556.

  • JoyID: signPsbt always returns a PSBT. JoyID is asked not to finalize, and CCC finalizes the inputs JoyID signed.
  • Replace bitcoinjs-lib with @scure/btc-signer in joy-id, xverse and the playground, so JoyID no longer needs initEccLib.

Breaking (playground): bitcoin from @ckb-ccc/playground is gone. Use import * as btc from "@scure/btc-signer".

Tested on testnet with JoyID and Xverse.

@changeset-bot

changeset-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b882848

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@ckb-ccc/joy-id Patch
@ckb-ccc/xverse Patch
@ckb-ccc/ccc Patch
ckb-ccc Patch
@ckb-ccc/connector Patch
@ckb-ccc/connector-react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify

netlify Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for appccc ready!

Name Link
🔨 Latest commit b882848
🔍 Latest deploy log https://app.netlify.com/projects/appccc/deploys/6ad9f0b9b4eab241b6146c16
😎 Deploy Preview https://deploy-preview-581--appccc.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 40 (🔴 down 22 from production)
Accessibility: 98 (🟢 up 1 from production)
Best Practices: 92 (🔴 down 8 from production)
SEO: 99 (🟢 up 11 from production)
PWA: -
View the detailed breakdown and full score reports
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for apiccc ready!

Name Link
🔨 Latest commit b882848
🔍 Latest deploy log https://app.netlify.com/projects/apiccc/deploys/6ad9f0b9645e71f13638211e
😎 Deploy Preview https://deploy-preview-581--apiccc.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 82 (🔴 down 7 from production)
Accessibility: 100 (no change from production)
Best Practices: 100 (no change from production)
SEO: 95 (no change from production)
PWA: -
View the detailed breakdown and full score reports
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for liveccc ready!

Name Link
🔨 Latest commit b882848
🔍 Latest deploy log https://app.netlify.com/projects/liveccc/deploys/6ad9f0b9a92207ecf81fa703
😎 Deploy Preview https://deploy-preview-581--liveccc.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 10 (🔴 down 2 from production)
Accessibility: 88 (no change from production)
Best Practices: 92 (🔴 down 8 from production)
SEO: 100 (no change from production)
PWA: -
View the detailed breakdown and full score reports
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for docsccc ready!

Name Link
🔨 Latest commit b882848
🔍 Latest deploy log https://app.netlify.com/projects/docsccc/deploys/6ad9f0baa92207ecf81fa708
😎 Deploy Preview https://deploy-preview-581--docsccc.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 77 (🔴 down 15 from production)
Accessibility: 95 (no change from production)
Best Practices: 92 (🔴 down 8 from production)
SEO: 75 (no change from production)
PWA: -
View the detailed breakdown and full score reports
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@fghdotio
fghdotio force-pushed the refactor/scure-btc-signer branch from 3e9345b to dcaac2e Compare September 29, 2026 13:47
@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 93ed544d-71b0-42b3-9f38-f2f270b7cbea

📥 Commits

Reviewing files that changed from the base of the PR and between 9ffef01 and dcaac2e.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (17)
  • .changeset/joy-id-sign-psbt-return-psbt.md
  • .changeset/scure-btc-signer.md
  • packages/examples/package.json
  • packages/examples/src/playground/index.d.ts
  • packages/examples/src/transferBtc.ts
  • packages/joy-id/package.json
  • packages/joy-id/src/btc/index.test.ts
  • packages/joy-id/src/btc/index.ts
  • packages/joy-id/src/btc/psbt.ts
  • packages/joy-id/tsdown.config.mts
  • packages/playground/package.json
  • packages/playground/src/app/components/Editor.tsx
  • packages/playground/src/app/execute/index.tsx
  • packages/xverse/package.json
  • packages/xverse/src/signer.test.ts
  • packages/xverse/src/signer.ts
  • packages/xverse/tsdown.config.mts
💤 Files with no reviewable changes (1)
  • packages/examples/src/playground/index.d.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Summary

Summary by CodeRabbit

  • Bug Fixes

    • JoyID signing now returns a PSBT consistently, including when automatic finalization is requested. Eligible signed inputs are finalized locally, while inputs that other signers must handle can remain unsigned.
    • JoyID and Xverse better preserve unknown PSBT fields and handle transactions with inputs that lack signing or UTXO information.
  • Updates

    • Bitcoin examples and the playground now use the Scure Bitcoin signer. The playground’s bitcoin global is no longer available; import from @scure/btc-signer instead.

The PR replaces bitcoinjs-lib PSBT handling with @scure/btc-signer in JoyID and Xverse. JoyID now requests unfinalized PSBTs and can finalize signed inputs locally. The playground and BTC transfer example also use the Scure signer.

Changes: Bitcoin PSBT handling

Layer / File(s) Summary
JoyID PSBT parsing and finalization
packages/joy-id/src/btc/psbt.ts
New helpers validate JoyID’s returned PSBT against the original, detect new signatures, and finalize selected inputs. Finalization errors include guidance for partial or multisig signing.
JoyID signing flow and tests
packages/joy-id/src/btc/index.ts, packages/joy-id/src/btc/index.test.ts, packages/joy-id/package.json, packages/joy-id/tsdown.config.mts, .changeset/joy-id-sign-psbt-return-psbt.md
JoyID requests an unfinalized PSBT and locally finalizes signed inputs when requested. Tests cover input types, requested-input handling, unknown fields, and invalid responses. The package adds Scure dependencies and CommonJS bundling entries.
Xverse Scure PSBT handling
packages/xverse/src/signer.ts, packages/xverse/src/signer.test.ts, packages/xverse/package.json, packages/xverse/tsdown.config.mts, .changeset/scure-btc-signer.md
Xverse replaces bitcoinjs-lib PSBT operations with Scure transactions. Input detection, local finalization, tests, dependencies, and CommonJS bundling entries are updated.
Playground and example integration
packages/examples/package.json, packages/examples/src/playground/index.d.ts, packages/examples/src/transferBtc.ts, packages/playground/package.json, packages/playground/src/app/components/Editor.tsx, packages/playground/src/app/execute/index.tsx
The BTC transfer example builds its PSBT with @scure/btc-signer. The playground loads Scure declarations and libraries, and access to the removed bitcoin global raises an error directing scripts to the Scure signer.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant BitcoinSigner
  participant JoyID
  participant parseSignedPsbt
  participant finalizeSignedInputs
  BitcoinSigner->>JoyID: Request PSBT with autoFinalized false
  JoyID-->>BitcoinSigner: Return signed PSBT
  BitcoinSigner->>parseSignedPsbt: Validate response against original PSBT
  parseSignedPsbt-->>BitcoinSigner: Return parsed transactions
  opt Local finalization requested
    BitcoinSigner->>finalizeSignedInputs: Finalize requested inputs with new signatures
    finalizeSignedInputs-->>BitcoinSigner: Return finalized PSBT hex
  end
Loading

Suggested reviewers: hanssen0

Merge Risk: ⚪ Minimal · up to dcaac

The PSBT signer migration looks consistent, and no concrete merge-blocking issue was found. Live JoyID wallet behavior was not exercised against the tests' mocked responses, which is normal pre-release validation.

Security architecture risk: 🟡 Moderate · up to dcaac

The new signing flow checks that JoyID returns the same unsigned transaction, but it does not establish that all other PSBT data is unchanged. The signer migration may also remove extension fields when a PSBT is finalized locally. These are transaction-scoped risks; the available evidence does not establish an exploit or a broader service impact.

Retained concerns

  • Medium · security · inferred: The new default JoyID path can return a PSBT for downstream use after checking only unsigned-transaction equality. A wallet response that changes other input metadata or arrives already finalized is not rejected by that check; the effect on a subsequent signer is unverified.
  • Medium · reliability · inferred: Local auto-finalization in JoyID and Xverse parses with options that ignore unknown and proprietary PSBT fields, then serializes the parsed transaction. Extension data may therefore be absent from the returned PSBT, unlike the raw-response path; whether consumers rely on that data is unverified.
Security review details

Security Blast Radius

  • inferred — The direct exposure is a PSBT returned to a caller of a connected BTC signer, potentially including a later cosigner. The available paths do not show a new broadcast permission or service-wide privilege.

Security Findings and Attack Paths

  • inferred — A compromised or unexpectedly behaving wallet response could preserve the unsigned transaction while changing other PSBT data that the new default return path passes on. Whether such a change can defeat a downstream signer’s controls is not established.

Trust Boundaries and Controls

  • observed — JoyID’s wallet-response boundary now checks PSBT format and unsigned-transaction identity before returning a result. It does not independently authenticate the source of each newly detected signature entry.

Resilience and Maintainability Implications

  • observed — JoyID does not serialize a partially locally finalized result after an input-finalization error; callers can instead request the unfinalized response.

Hardening Proposals

  • proposed — Define which wallet-returned PSBT fields may change, including per-input finalization state, before a result is passed to another signer; verify or reject changes outside that contract.
  • proposed — Establish whether callers require unknown or proprietary PSBT fields, then preserve them or explicitly reject unsupported PSBTs on paths that reserialize them.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.17% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 24 functions across 10 files. (6 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes both primary changes: JoyID now returns a PSBT from signPsbt, and the codebase switches to @scure/btc-signer.
Description check ✅ Passed The description explains the main changes, breaking playground change, referenced issues, and test coverage. It does not include the template’s contributing-guidelines checklist, but the description i…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 29.17% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 24 functions across 10 files. (6 skipped: 6 unsupported.)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@fghdotio fghdotio changed the title refactor(btc): replace bitcoinjs-lib with @scure/btc-signer fix(joy-id): return a PSBT from signPsbt, and switch to @scure/btc-signer Sep 29, 2026
@fghdotio
fghdotio marked this pull request as ready for review September 29, 2026 13:50
@fghdotio fghdotio closed this Sep 29, 2026
@fghdotio fghdotio reopened this Sep 29, 2026
@Hanssen0

Copy link
Copy Markdown
Member

/canary

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Canary version published successfully! View workflow run

The following packages have been published to npm:

  • @ckb-ccc/joy-id@0.0.0-canary-dcaac2eb7549f82887bdeac3ede8b556ba9ba3d2
  • @ckb-ccc/xverse@0.0.0-canary-dcaac2eb7549f82887bdeac3ede8b556ba9ba3d2
  • @ckb-ccc/ccc@0.0.0-canary-dcaac2eb7549f82887bdeac3ede8b556ba9ba3d2
  • ckb-ccc@0.0.0-canary-dcaac2eb7549f82887bdeac3ede8b556ba9ba3d2
  • @ckb-ccc/connector@0.0.0-canary-dcaac2eb7549f82887bdeac3ede8b556ba9ba3d2
  • @ckb-ccc/connector-react@0.0.0-canary-dcaac2eb7549f82887bdeac3ede8b556ba9ba3d2

Hanssen0-Alter-Ego commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Generated by ChatGPT · Alter Ego

Updated 2026-10-16 at unchanged head dcaac2eb7549f82887bdeac3ede8b556ba9ba3d2. The migration retains ordinary Taproot key-path and two signature-only script templates, improves standard threshold witness assembly, and narrows automatic finalization of other scripts. The detailed compatibility clarification below remains a non-blocking support-scope discussion; the former custom-script P2 stays withdrawn.

Retained inline findings:

These remain in the existing first COMMENT review and additional P3 review. This edit does not duplicate or change those findings.

Reassessment preserved: I overstated both original compatibility findings as P2. The non-template Taproot P2 is withdrawn. Mixed bare inputs are P3, confined to bare P2PK/multisig. No affected CCC production downstream was established; that does not establish that none exists.

2026-10-16 compatibility clarification: exact scope, matrix, examples, and limits

Which behavior is being compared?

The audit used the exact locked bitcoinjs-lib 7.0.2 and @scure/btc-signer 2.4.1, with base 9ffef01a181751dacf1edde83ed43fcfb4646ded and the head above.

CCC configures { unknown: "ignore", proprietary: "ignore" }. These preserve unknown/proprietary PSBT fields; they do not enable allowUnknownInputs or custom Taproot finalizer callbacks. Options on a caller's own scure instance are not serialized into the PSBT and inherited by CCC.

What the default finalizers actually do

Old bitcoinjs: requires witnessUtxo for its Taproot finalizer; uses tapKeySig first. Otherwise, it picks the shortest control-block leaf having at least one matching leaf-hash signature, sorts that leaf's existing signatures by public-key position in the script, and appends the original script and control block. It has no general script solver or completeness check. It cannot invent a preimage, branch selector, annex, missing multisig slots, or repeated-key stack entries. Returning a witness does not prove a valid spend. The audited source was the 7.0.2 npm package, integrity-checked against the base lock: src/esm/psbt.js:351–378, psbt/bip371.js:408–473, psbt/psbtutils.js:63–84.

New scure under CCC's options: after PSBT/prevout/commitment checks, tapKeySig still takes priority. Script-path candidates must match one of these exact decoded templates:

Template Shape Required satisfaction
tr_ns <K1> CHECKSIGVERIFY … <Kn> CHECKSIG 1 ≤ n ≤ 999; every key occurrence has a signature for this leaf
tr_ms <K1> CHECKSIG <K2> CHECKSIGADD … <Kn> CHECKSIGADD <m> NUMEQUAL 1 ≤ m ≤ n ≤ 999; enough signed key occurrences to fill exactly m positions

Each K must be a valid 32-byte x-only public key. tr_ns with n=1 is <K> CHECKSIG. The threshold m is not limited to 16: a minimally encoded ScriptNum of up to four bytes also works. Non-minimal push opcodes for keys can still decode to a recognized template; non-minimal numeric encoding of m is a different case.

The finalizer fills per-key slots, reverses them for witness order, appends the original script/control block, and chooses the smallest complete serialized witness. Repeated keys can reuse the same signature at each occurrence. Sources: template recognition, candidate assembly and selection.

Compatibility matrix

Case Old local assembly New default local assembly
Ordinary key-path / single CHECKSIG Supported Retained, subject to parsing; optional tree metadata is the separate P3 above
N-of-N signature chain Can emit an incomplete witness Requires all key positions
M-of-N CHECKSIGADD … NUMEQUAL Does not add empty slots or trim excess signatures Adds empty slots and uses exactly m signed positions
Repeated keys / multiple candidate leaves Each PSBT signature emitted once; shallowest leaf with any signature Reuses signature per occurrence; selects a complete, smallest witness
CLTV/CSV wrappers, inscription envelopes, equivalent boolean/threshold rewrites Can assemble when supplied signatures alone form the required stack Excluded unless another supported leaf has a complete satisfaction
Parseable unsupported leaf alongside a complete supported leaf Selection depends on old leaf order Supported candidate can succeed
Undecodable unused leaf alongside a complete supported leaf Unselected leaf need not block assembly Decode exception can abort the whole script-path finalization
Taproot nonWitnessUtxo-only / PSBTv2 Old Taproot finalizer / old Xverse parser rejects, respectively Passed the isolated comparisons
External ECC initialization Needed by old bitcoinjs Taproot payment construction No external initialization needed

For example, 1-of-2 with only A signed changes from [sigA, script, cb] to [empty, sigA, script, cb]. If A and B both sign, the new finalizer still uses one signature and leaves the other slot empty. These are genuine improvements.

The excluded family is broader than one or two examples:

  • <t> CHECKLOCKTIMEVERIFY DROP <K> CHECKSIG and <delay> CHECKSEQUENCEVERIFY DROP <K> CHECKSIG
  • <K> CHECKSIGVERIFY TRUE, <K> CHECKSIG VERIFY TRUE, or boolean wrappers
  • <K> CHECKSIG FALSE IF <inscription payload> ENDIF
  • Zero-initialized CHECKSIGADD, NUMEQUALVERIFY TRUE, or GREATERTHANOREQUAL threshold variants

This is a structural boundary, not a claim that every custom script was previously spendable. Missing preimages/selectors, insufficient signatures, invalid commitments, disabled tapscript CHECKMULTISIG, and stack-limit violations must not be counted indiscriminately as lost valid support. CODESEPARATOR also needs separately correct signature generation; old witness assembly alone does not establish it.

An unsupported but parseable leaf can be skipped. An unused leaf with an unknown opcode or truncated push can instead throw during OutScript.decode, even after another candidate was found. Thus “any custom leaf breaks the whole tree” would overstate the result.

CLTV/CSV: what makes the examples meaningful?

The audit used <500> CLTV DROP <K> CHECKSIG and <5> CSV DROP <K> CHECKSIG, with transaction version 2, locktime 600, sequence 5, leaf version 0xc0, correct Taproot commitments, and real synthetic Schnorr signatures that verified. Old assembly produced [signature, script, controlBlock]; new default finalization returned Unknown tapLeafScript.

For CLTV, the operand must be a valid nonnegative ScriptNum, match the height/time type of nLockTime, be ≤ nLockTime, and the input sequence must be non-final. For an active CSV relative-lock branch, the transaction version must be ≥2, neither relevant disable bit may be set, type bits must match, and the masked operand must be ≤ the input sequence. A CSV operand with its disable bit set has different NOP semantics.

Actual inclusion additionally requires the appropriate absolute/relative maturity and all other transaction conditions. Both fixtures used version 2 and sequence 5, so transaction-level BIP68 maturity also applies to the CLTV example. These are code/spec-supported valid-satisfaction regressions under those conditions; no Bitcoin Core/full consensus interpreter or live transaction was run. See BIP65, BIP112, and BIP68.

Escape paths and actual caller impact

  • Xverse: explicit inputsToSign plus autoFinalized:false bypasses local parsing/finalization. Without explicit inputs, the initial default-input scan still parses. Source
  • JoyID: head requests an unfinalized PSBT, then parses both the original and returned PSBT and compares the unsigned transaction before honoring autoFinalized:false. This flag does not bypass parser restrictions. Local finalization selects requested/newly signed inputs; “new signature” is a byte comparison, not cryptographic verification. Adapter, parser and selection
  • Already-finalized inputs are skipped only after parsing succeeds. A final witness is not a universal parser bypass.
  • Signing and finalization are separate. Both CCC adapters obtain signatures from the wallet. In the audit, scure itself could generate signatures for several custom examples that its default finalizer rejected; this does not mean wallet signing was replaced or all such scripts are unsignable.
  • The current BTC transfer example has no custom leaf and uses wallet-side signAndBroadcastPsbt, avoiding the local finalizer discussed here. Xverse's default-input parse can still apply. Both adapters' standalone broadcastPsbt remains unsupported, so partial-PSBT return alone does not finish a custom-spend workflow.

The affected caller shape is an application supplying a custom Taproot PSBT, receiving unfinalized leaf signatures from its wallet, and relying on CCC's default signPsbt() to assemble the witness. Xverse's local regression is reproduced; JoyID's previous real-wallet scope and the production frequency remain unestablished. The API promises attempted finalization, so documenting the supported templates/exit path and adding representative tests is appropriate without reinstating the withdrawn P2.

Verification

A fresh isolated audit on 2026-10-16 exercised 61 fixture groups and 240 passing tests across five test files: head Xverse 8, head JoyID 22, base Xverse 6, base JoyID 3, plus 201 audit checks. These include expected-rejection assertions; they do not mean every fixture finalized successfully. Actual snapshot adapter/finalizer sources were imported with mocked wallet provider/popup responses. Five custom-script families included verified synthetic Schnorr signatures.

Exact package tarballs were checked against lockfile integrity. The scure 2.4.1 source matched its published package. This was not a full repository build/lint, live-wallet E2E, consensus-interpreter run, or broadcast; no real accounts or funds were used. The parser metadata P3 above has its own narrower reproduction and remains separately tracked.

Earlier 2026-10-16 reassessment and verification record (preserved)

“Supported custom scripts” lacked a CCC contract basis: the API promises attempted finalization. Taproot reproductions used synthetic wallet responses. Xverse reachability is source-based, not wallet E2E; JoyID support remains unverified.

Tested ordinary payment, standard multisig, Taproot key-path/single-CHECKSIG and ordinary mixed-input flows work. Already-run independent tests: 30 changed-file + 4 regression + 46 follow-up checks passed, including reproduced-failure assertions and defensive prototypes. Synthetic/mocked coverage is not real-wallet E2E.

At that earlier edit, only the published reassessment was reorganized: no new tests or fixes were made. Full build/lint and browser integration were not independently exercised. The later audit above adds evidence without changing the severity correction.

@Hanssen0-Alter-Ego Hanssen0-Alter-Ego 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.

Generated by ChatGPT · Alter Ego

Moved the two retained findings from the earlier summary to the relevant code lines at dcaac2eb7549f82887bdeac3ede8b556ba9ba3d2: one P2 signature-hash consistency issue and one narrowly scoped P3 bare-input compatibility issue. The non-template Taproot P2 remains withdrawn.

This is a presentation update of the existing reassessment. No new tests or code changes were made; synthetic/mocked coverage is not real-wallet E2E, and full build/lint and browser integration were not independently exercised.

Comment thread packages/xverse/src/signer.ts Outdated
Comment on lines +27 to +29
const scratch = new Transaction(PSBT_OPTIONS);
scratch.addInput(input);
scratch.finalizeIdx(0);

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.

Generated by ChatGPT · Alter Ego

[P2] Reject a signature whose hash mode disagrees with sighashType

When local auto-finalization is enabled and the wallet returns an ECDSA signature whose hash mode differs from the input’s retained sighashType, this helper finalizes it and then clears that declaration. The previously reproduced mismatch was rejected by old Xverse but accepted by the new Xverse/JoyID helpers; if the returned mode commits to less of the transaction, the finalized result carries weaker commitments than declared. BIP174 requires finalizers to reject this mismatch.

Suggested fix

Check each signature’s actual hash mode against the declaration before calling finalizeIdx or clearing metadata. Cover ECDSA and Schnorr encodings, and compare against the original request’s policy while preserving explicitly allowed modes; the latter also hardens existing gaps.

Apply the same guard to JoyID’s mirrored helper. The reproduced comparison was ECDSA; Schnorr coverage is a fix recommendation, not a claimed tested regression. No live-wallet exploit was demonstrated, and old JoyID behavior remains unverified.

Comment thread packages/xverse/src/signer.ts Outdated
Comment on lines +384 to +387
const signedPsbt = Transaction.fromPSBT(
ccc.bytesFrom(signedPsbtBase64, "base64"),
PSBT_OPTIONS,
);

@Hanssen0-Alter-Ego Hanssen0-Alter-Ego Oct 16, 2026 •

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.

Generated by ChatGPT · Alter Ego

[P3] Keep an unrelated bare input from blocking selected-input finalization

If a PSBT contains a selected standard input plus another signer’s bare P2PK or bare multisig input with UTXO data, this whole-PSBT parse rejects the bare input before finalizeSignedInputs can skip it. The selected input therefore cannot be returned finalized even though the bare input was not requested for signing. This is a narrow compatibility issue: no affected CCC downstream was found.

Suggested fix

Separate unrelated-input passthrough from local finalizer support while retaining script-commitment checks, or explicitly document these bare-input PSBTs as unsupported.

The scure 2.4.1 parser validates every supplied prevout and rejects non-wrapped pk/ms scripts. The default-input scan has the same restriction. JoyID parses both PSBTs before checking autoFinalized, so disabling finalization does not bypass its parser.

This does not apply to the previously tested ordinary payment, wrapped-multisig, Taproot key-path/single-CHECKSIG, or ordinary mixed-input flows, which passed. The compatibility checks used synthetic/mocked responses; JoyID’s live-wallet support for these bare-input cases remains unverified.

2026-10-22 correction / follow-up

At 2c45ab669e67ec32b932e5505ccfd86e5e932c6d, the original bare-input compatibility failure described above no longer occurs: both P2PK and bare multisig cases now pass in Xverse and JoyID, with witness or non-witness UTXO metadata. The selected standard input finalizes while the unrelated input is preserved; the parent commit reproduces the rejection. JoyID's partial-return path now accepts these PSBTs too. Xverse's explicit-input partial-return bypass was already working before this fix.

The full correction is only partly complete because the earlier requirement to retain supplied-commitment checks is unmet. That remaining problem is tracked once in the new selected-input validation finding; this thread stays unresolved. These were isolated adapter tests with mocked wallet boundaries, not a full repository suite/build or live-wallet E2E. The original production-impact and live JoyID support limits still apply.

2026-10-22 correction / verification at b882848aafa9dabdf3cb1348d4af6b58c87b0a89

The scoped correction is now verified. Both adapters preserve unrelated bare P2PK and multisig inputs with witness or non-witness UTXO metadata while completing the selected standard input. Partial-return controls also pass. Selected-input commitment checks are restored and reject the tested conflicts, addressing the remaining qualification in the earlier follow-up. This finding is fully fixed within the verified scope.

Tests used synthetic PSBTs, isolated actual adapter sources, and mocked wallet boundaries with existing compiled support and dependency aliases. This was not a fresh frozen-lockfile installation or full monorepo build/suite, live-wallet E2E, or Bitcoin consensus execution. No production caller impact or live JoyID support is established.

Generated by ChatGPT · Alter Ego

@Hanssen0-Alter-Ego Hanssen0-Alter-Ego 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.

Generated by ChatGPT · Alter Ego

One additional, independently reproduced P3 compatibility finding at dcaac2eb7549f82887bdeac3ede8b556ba9ba3d2, anchored inline. The earlier severity corrections remain unchanged. Verification used isolated synthetic fixtures and method-body harnesses, not real-wallet E2E; no production downstream impact was established.

// Collect all unsigned inputs
const psbt = Psbt.fromHex(psbtHex.slice(2));
psbt.data.inputs.forEach((input, index) => {
const tx = Transaction.fromPSBT(ccc.bytesFrom(psbtHex), PSBT_OPTIONS);

@Hanssen0-Alter-Ego Hanssen0-Alter-Ego Oct 16, 2026 •

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.

Generated by ChatGPT · Alter Ego

[P3] Accept presigned Taproot inputs with undisclosed tree metadata

An already-signed tree-backed Taproot input can retain the correct tapInternalKey while omitting tapMerkleRoot and tapLeafScript. These fields are optional under BIP371, but scure 2.4.1 treats this combination as an empty-tree commitment and rejects the entire PSBT here.

At dcaac2eb7549f82887bdeac3ede8b556ba9ba3d2, an isolated synthetic reproduction with the exact locked versions verified the key-path signature against the output key. The base default scan skipped that signed input and selected a separate unsigned P2WPKH input; the head scan throws before contacting the wallet. The returned-PSBT parser also rejects this case. This is a key-path metadata compatibility issue, distinct from the withdrawn custom-script finding. No affected production caller or live-wallet behavior was established; the method-body harness used utility/schema/provider stubs, not a full repository build or wallet E2E.

Suggested fix

Handle undisclosed tree metadata without assuming an empty tree, while retaining checks for supplied commitments. Cover both Xverse parse sites and JoyID's parsing.

When a response omits metadata present in the original request, restore only matching, validated original metadata after checking transaction/input identity and prevout consistency; reject conflicts. Restoring the verified original root passed the synthetic control, with wrong-root rejection still enabled. This does not cover an original PSBT already missing the root: that needs targeted parser compatibility for signed key-path inputs, using the output key for signature validation rather than globally disabling script checks.

2026-10-22 correction / follow-up

At 2c45ab669e67ec32b932e5505ccfd86e5e932c6d, the original presigned key-path compatibility failure described above no longer occurs. Both adapters now accept a request already omitting optional tree metadata and a response omitting metadata present in the request. The synthetic key-path signature verifies against the output key, the presigned input is preserved, and the separate selected standard input completes. The before/after checks cover explicit/default selection and finalization on/off; Xverse's explicit partial-return bypass remains a pre-existing control.

The full correction is only partly complete: supplied conflicting commitments are now accepted too, so the earlier requirement to retain those checks remains unmet. See the separate selected-input validation finding for the new regression; this thread stays unresolved. The custom-script support finding remains withdrawn. Coverage is isolated actual adapter sources with mocked boundaries; the full repository suite/build and live-wallet E2E were not run, and no production caller impact or live-wallet exploit is established.

2026-10-22 correction / verification at b882848aafa9dabdf3cb1348d4af6b58c87b0a89

[P3] Preserve selected Taproot key-path compatibility when optional tree metadata is omitted

The separate presigned, unselected-input cases from the earlier follow-up still pass: the presigned input remains unchanged and unfinalized while the separate selected standard input completes. However, the new strict scratch parser at Xverse lines 35–36 and JoyID lines 22–23 rejects a selected, tree-backed key-path input retaining the correct tapInternalKey and a valid output-key signature when optional tapMerkleRoot/tapLeafScript metadata is omitted. Scure interprets that incomplete metadata as an empty-tree commitment. As a result, local auto-finalization throws instead of returning the valid selected input finalized.

Both adapters finalize the identical selected-input fixtures at the previous head, 72d47bfbe9d3e314266e8f2e5115684d1898daca, but reject them at this head. Retaining the correct root or setting autoFinalized: false succeeds. The checks cover explicit/default selection, a request containing the root whose response omits it, and a request already lacking the root. Thus the prior unselected-input correction still holds, but selected-input compatibility regressed; this thread remains open.

Suggested fix

Handle omitted-tree key-path metadata selectively while retaining supplied-commitment validation. Where a response omits metadata present in the request, consider restoring only matching, validated request metadata after verifying transaction/input identity and prevout consistency. Requests already lacking the root need targeted key-path compatibility using the output key, without assuming an empty tree. Preserve rejection of supplied conflicting commitments; do not globally disable script checks again. This is a proposed correction, not a tested patch.

Evidence uses synthetic, independently output-key-verified signatures and isolated actual adapter sources with mocked wallet boundaries, existing compiled support, and dependency aliases. It demonstrates adapter compatibility, not a wallet's ability to discover an undisclosed root. No production caller or live-wallet impact was established. The full monorepo build/suite, live-wallet E2E, and Bitcoin consensus execution were not run.

Generated by ChatGPT · Alter Ego

const PSBT_OPTIONS: TxOpts = {
unknown: "ignore",
proprietary: "ignore",
disableScriptCheck: true,

@Hanssen0-Alter-Ego Hanssen0-Alter-Ego Oct 22, 2026 •

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.

Generated by ChatGPT · Alter Ego

[P2] Keep commitment checks on inputs being finalized

This option is also used by the scratch transaction in finalizeInput, so it disables commitment checks on selected inputs, beyond allowing unrelated-input passthrough. At 2c45ab669e67ec32b932e5505ccfd86e5e932c6d, isolated before/after tests show both adapters returning finalized witnesses for selected standard inputs with conflicting script commitments that the parent commit rejects. Those witnesses cannot satisfy their declared prevouts.

Suggested fix

Keep compatibility parsing separate from validation of selected inputs and their scratch-finalization path; validate supplied commitments before clearing metadata. Apply the same change to JoyID's mirrored option, and add negative selected-input commitment cases alongside the positive compatibility tests. This is a proposed correction, not a tested patch.

Verification: 111 isolated case groups passed, including expected-rejection and defect-reproduction assertions against actual before/after adapter sources with mocked boundaries. Matching-commitment controls pass. The full repository suite/build, live-wallet E2E, and Bitcoin consensus interpreter were not run. This is a reproduced witness-correctness regression; no live-wallet exploit or fund loss is claimed.

2026-10-22 correction / verification at b882848aafa9dabdf3cb1348d4af6b58c87b0a89

Independent verification confirms the fix for this finding. Both adapters now reject the tested supplied-commitment conflicts before local finalization; matching controls succeed. The previous head, 72d47bfbe9d3e314266e8f2e5115684d1898daca, accepts those conflicting fixtures. Partial-return behavior remains available when finalization is disabled.

Coverage includes representative P2SH, P2WSH, nested P2SH-P2WSH, Taproot internal-key/root/control-block, and stray script-metadata conflicts. These are representative supplied-commitment checks, not exhaustive metadata-permutation coverage. This finding is fully fixed within the verified scope.

The separate omitted-tree compatibility regression introduced by strict scratch parsing is tracked in the existing omitted-metadata thread, which remains open.

Validation comprised 290 isolated expected-outcome case groups, including defect reproductions and controls, using synthetic PSBTs and actual adapter sources with mocked wallet boundaries and existing compiled support. Separately, the two repository test files passed 42 tests at this head and 39 at the previous head using dependency aliases. This was not a fresh frozen-lockfile installation or package-export/build test. The full monorepo build/suite, live-wallet E2E, and Bitcoin consensus execution were not run.

Generated by ChatGPT · Alter Ego

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in b882848.

@fghdotio
fghdotio force-pushed the refactor/scure-btc-signer branch from 2c45ab6 to 72d47bf Compare October 22, 2026 09:43

This branch has not been deployed

No deployments
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