Skip to content

feat(cli): add sign-digest command for Taproot multisig#11

Closed
aetos53t wants to merge 5 commits intotiero:mainfrom
aetos53t:feat/sign-digest-command
Closed

feat(cli): add sign-digest command for Taproot multisig#11
aetos53t wants to merge 5 commits intotiero:mainfrom
aetos53t:feat/sign-digest-command

Conversation

@aetos53t
Copy link
Contributor

Summary

Adds cash sign-digest <hex> command to sign raw 32-byte digests with the wallet's Schnorr key (BIP-340).

Changes

  • cli/src/commands/sign-digest.ts - New command handler
  • cli/src/index.ts - Register command + help text
  • cli/src/server.ts - Daemon route POST /sign-digest

Usage

cash sign-digest abc123...64chars
cash sign-digest --hex 0xabc123...

Returns:

{
  "digest": "abc123...",
  "signature": "def456...",
  "publicKey": "789...",
  "note": "64-byte BIP-340 Schnorr signature"
}

Use Case

Multi-agent Taproot multisig coordination:

  1. Coordinator computes BIP-341 sighash for each input
  2. Each agent calls cash sign-digest <sighash>
  3. Coordinator collects signatures, assembles witness with OP_CHECKSIGADD
  4. Broadcast transaction

This enables AI agent treasuries and multi-party custody where different Claw Cash identities participate in shared Bitcoin wallets.

Testing

cd cli && pnpm run typecheck  # passes

@aetos53t aetos53t force-pushed the feat/sign-digest-command branch from 594f2ab to 83e6129 Compare February 18, 2026 22:39
Add 'cash sign-digest <hex>' command that signs a raw 32-byte digest
with the wallet's Schnorr key (BIP-340).

CLI Command (cli/src/commands/sign-digest.ts):
- Input validation (hex format, 64 char length)
- Normalization (strip 0x prefix, lowercase)
- Support for positional arg and --hex/--digest flags
- Detailed error messages with usage hints
- Daemon fallback when not running
- Proper ClwApiError handling

Daemon Route (cli/src/server.ts):
- POST /sign-digest endpoint
- Same validation as CLI
- Consistent response format with signatureFormat field

Unit Tests (test/sign-digest.test.ts):
- Missing digest rejection
- Invalid hex rejection
- Wrong length rejection (too short/long)
- 0x prefix handling
- --hex and --digest flag support
- Digest normalization
- Signature format validation

E2E Tests (test/e2e.test.ts):
- signDigest() combines sign-intent + sign in one call
- signDigest() signature is valid BIP-340 Schnorr (cryptographic verification)
- signDigest() with 0x prefix works
- signDigest() with invalid digest length throws

Usage:
  cash sign-digest e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
  cash sign-digest --hex 0xabc123...

Returns:
  {
    "digest": "...",
    "signature": "...",
    "publicKey": "...",
    "signatureFormat": "BIP-340 Schnorr (64 bytes)"
  }

Use case: Multi-agent Taproot multisig coordination where each agent
signs their BIP-341 sighash independently, then a coordinator
assembles the witness with OP_CHECKSIGADD.

All 61 tests pass (including cryptographic signature verification).
@aetos53t aetos53t force-pushed the feat/sign-digest-command branch from 83e6129 to 49040f0 Compare February 18, 2026 22:42
Tests:
- Replace stub tests with complete assertions
- Add input validation tests (missing, invalid, wrong length)
- Add digest normalization tests (0x prefix, uppercase, mixed case)
- Add input parsing tests (positional, --hex, --digest flags)
- Add BIP-340 format validation tests
- Add daemon route integration tests

Docs:
- Add sign-digest to command table
- Add dedicated section with usage examples
- Document output format and use cases
@tiero
Copy link
Owner

tiero commented Feb 19, 2026

This is surely something I'm open to offer, but we have to think here about blind signing issues. I wish to guardrails as much as possible the meaning of signature coming from agent discretion.

If I understand you looking into taproot multisigs.

I feel PSBT is a better medium to share "what is being signed" in a standardized way along with attaching co-signer signature for specific script path in the taproot tree. This can be paired with both agent and policies directly inside the low-lever identity (in this case private keys inside a Secure Enclave)

Unless you have a very custom need, happy to discuss!

@aetos53t
Copy link
Contributor Author

You're right - the sign-digest command alone enables blind signing, which is a valid concern.

Context: I'm building a multi-agent coordination API for Taproot multisigs. The flow is:

  1. Coordinator creates PSBT (full tx visible to all signers)
  2. Each agent receives PSBT, validates inputs/outputs independently
  3. Agent computes their BIP-341 sighash FROM the validated PSBT
  4. Agent signs that specific digest
  5. Coordinator collects signatures, assembles witness

So the PSBT is the source of truth - sign-digest is the mechanical step after validation, not a replacement for it. The coordination layer handles the "what is being signed" visibility you're describing.

That said, for the CLI in isolation, your caution is warranted. Would it help if I added a sign-psbt wrapper that:

  • Accepts a PSBT
  • Displays the transaction details (inputs, outputs, amounts)
  • Extracts the sighash for the relevant input
  • Calls sign-digest internally
  • Returns the partially-signed PSBT

This way the CLI is safe by default, with sign-digest available as an advanced primitive for coordination protocols that handle validation externally.

Happy to discuss the architecture further - the goal is enabling MuSig2-style coordination where multiple agents each hold a key share.

@tiero
Copy link
Owner

tiero commented Feb 20, 2026

Yep if we can move to sign-psbt would be ideal.

We use https://github.com/paulmillr/scure-btc-signer as bitcoin library (pure JS, audited) and has MuSig2 available.

It should be straightforward to add the decode of the PSBT and then perform the signing with the RemoteSignerIdentity as we do now.

For Arkade-specific multi-party contract an example you can look at is the escrow

For Bitcoin onchain contracts it should be straightforward instead with just plain PSBT

Addresses feedback from tiero - adds a safer alternative to sign-digest:
- Parses PSBT to show transaction details before signing
- Computes sighash automatically for each input
- Returns updated PSBT with signatures added
- Uses same scure-btc-signer library

The sign-psbt command is recommended over sign-digest for production use
as it prevents blind signing attacks.
@aetos53t
Copy link
Contributor Author

Added sign-psbt command! ✅

This addresses your blind signing concern:

cash sign-psbt <base64-psbt>

What it does:

  1. Parses the PSBT and displays transaction details (inputs, outputs, amounts, fee)
  2. Identifies inputs where the wallet's pubkey is a signer
  3. Computes sighash automatically for each input using @scure/btc-signer
  4. Signs and returns the updated PSBT with signatures

Example output:

{
  "summary": {
    "inputsTotal": 1,
    "outputsTotal": 2, 
    "fee": "1060 sats",
    "inputsSigned": 1
  },
  "signatures": [{"inputIndex": 0, "signature": "..."}],
  "psbt": {"base64": "...", "hex": "..."},
  "note": "PSBT updated with signatures..."
}

This way agents can see exactly what they're signing before signing. The sign-digest command is still available for low-level use cases, but sign-psbt is the recommended approach.

Using the same @scure/btc-signer library you mentioned. MuSig2 would be a natural next step!

@tiero
Copy link
Owner

tiero commented Feb 21, 2026

I think it's good progress.

Let's remove the sign-digest to avoid consumer to be "tempted" to use unsafe methods.

Add a unit test for sign-psbt with some vectors and use the method in full scenario during e2e tests

Per tiero's feedback:
- Remove sign-digest CLI command (prevents blind signing temptation)
- Remove /sign-digest daemon route
- Keep SDK signDigest method (used internally by sign-psbt)
- Add comprehensive sign-psbt unit tests with test vectors
- Add sign-psbt e2e tests for full scenario testing

sign-psbt is the recommended way to sign - it parses the PSBT,
shows transaction details, and prevents blind signing attacks.
@aetos53t
Copy link
Contributor Author

Done! Addressed your feedback:

Removed sign-digest - CLI command and daemon route both removed to prevent blind signing temptation

Added sign-psbt unit tests with test vectors:

  • PSBT parsing (base64 and hex)
  • Taproot script-path detection
  • Sighash computation tests
  • Input validation tests
  • BIP-340 signature format validation
  • PSBT magic bytes verification

Added sign-psbt e2e tests:

  • Full scenario: create PSBT → parse → sign → verify output format
  • Transaction info extraction
  • Sighash computation for Taproot inputs
  • Output format validation

The SDK's signDigest method is still available internally (sign-psbt uses it), but consumers can only access the safe sign-psbt command that shows full transaction details before signing.

| `cash swaps` | List swaps (last 5 per category) |
| `cash claim <id>` | Manually claim a swap (reveal preimage) |
| `cash refund <id>` | Manually refund a swap |
| `cash sign-digest <hex>` | Sign a raw 32-byte digest with Schnorr (BIP-340) |
Copy link
Owner

Choose a reason for hiding this comment

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

Remove it from here and add sign-psbt?

import { outputSuccess, outputError } from "../output.js";
import { getDaemonUrl, daemonPost } from "../daemonClient.js";
import { loadConfig, validateConfig } from "../config.js";
import { ClwApiClient, ClwApiError } from "@clw-cash/sdk";
Copy link
Owner

Choose a reason for hiding this comment

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

Why not relative import?

@tiero
Copy link
Owner

tiero commented Feb 23, 2026

typecheck fails

- Add @scure/btc-signer and @scure/base as CLI dependencies
- Add @scure/btc-signer and @scure/base as root dev dependencies for tests
- Fix import path for tapLeafHash (@scure/btc-signer/payment.js)
- Update tapLeafScript access for v2 API (tuple format)
- Update tapScriptSig format for v2 API

All 68 tests pass.
@aetos53t
Copy link
Contributor Author

Fixed! ✅

The typecheck was failing due to btc-signer v2 API changes:

  • Added missing @scure/btc-signer and @scure/base dependencies
  • Fixed import path for tapLeafHash (now @scure/btc-signer/payment.js)
  • Updated tapLeafScript access for v2 tuple format [controlBlock, script]
  • Updated tapScriptSig format for v2 API

All 68 tests passing now. Ready for review! 🚀

@tiero
Copy link
Owner

tiero commented Feb 27, 2026

closed in favor of #15

@tiero tiero closed this Feb 27, 2026
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