Skip to content

fix(utxo-lib): honor caller hashType for SmartTransaction signatures (atomic-swap offers)#6

Open
caribu66 wants to merge 1 commit into
VerusCoin:utxo-lib-verusfrom
caribu66:fix/smarttx-sighashtype-offer-signing
Open

fix(utxo-lib): honor caller hashType for SmartTransaction signatures (atomic-swap offers)#6
caribu66 wants to merge 1 commit into
VerusCoin:utxo-lib-verusfrom
caribu66:fix/smarttx-sighashtype-offer-signing

Conversation

@caribu66

@caribu66 caribu66 commented Jul 8, 2026

Copy link
Copy Markdown

Summary

TransactionBuilder treated SmartTransaction (VerusID / CryptoCondition) signatures as if they were always SIGHASH_ALL, which made atomic-swap offers — signed SIGHASH_SINGLE | ANYONECANPAY so a taker can complete them — impossible to build or verify. This fixes both the signing and the mutability guards.

1. sign() — declare the real hashtype in the blob

The SmartTransactionSignatures blob hardcoded sigHashType = 1 while the digest was computed with the caller-supplied hashType. Any non-ALL smart signature therefore verified against the wrong digest and the completed transaction was rejected (mandatory-script-verify-flag-failed, false/empty top stack).

2. __canModifyInputs / __canModifyOutputs — read the blob header, not a DER byte

Both guards derived the hashtype from the trailing byte of a DER signature, which is meaningless for a SmartTransactionSignatures blob. As a result, adding a takers inputs/outputs to a maker-signed offer threw No, this would invalidate signatures. They now parse the blob header via SmartTransactionSignatures.fromChunk:

  • inputs: ANYONECANPAY permits appending more inputs.
  • outputs: SIGHASH_SINGLE permits appending outputs once the paired (same-index) output exists (vin < nOutputs); SIGHASH_NONE always permits.

Applied to both src/ and dist/src/.

Why it matters

This is what lets a wallet act as an atomic-swap taker entirely client-side: keep the makers SINGLE|ANYONECANPAY identity input untouched, add the buyers funding inputs + the transferred identity output + change, sign only the buyer inputs, and broadcast.

Testing

Proven live on VRSCTEST via the Verus Mobile marketplace flows: a maker offer signed SINGLE|ANYONECANPAY is completed by a taker on a second device; the swap settles on-chain and the identity transfers to the buyer with revocation/recovery authorities preserved. Two independent on-device swaps across two phones passed first-try.

Downstream (Verus-Mobile) currently carries this as a patch-package patch; merging here lets that patch be dropped.

TransactionBuilder produced and validated SmartTransaction (identity/CC)
signatures as if they were always SIGHASH_ALL, which broke atomic-swap
offers signed SIGHASH_SINGLE|ANYONECANPAY:

1. sign(): the SmartTransactionSignatures blob hardcoded sigHashType=1 while
   the digest was computed with the caller-supplied hashType, so any non-ALL
   smart signature verified against the wrong digest and the completed tx was
   rejected (mandatory-script-verify-flag / false-empty-stack).

2. __canModifyInputs / __canModifyOutputs read the hashtype from the trailing
   byte of a DER signature, which is meaningless for a SmartTransactionSignatures
   blob. Adding a taker's inputs/outputs to a maker-signed offer therefore threw
   "No, this would invalidate signatures". Both guards now parse the blob header
   via SmartTransactionSignatures.fromChunk: ANYONECANPAY permits extra inputs,
   and SIGHASH_SINGLE permits appending outputs once the paired (same-index)
   output exists.

Proven live on VRSCTEST: full self-custodial atomic swap (maker offer signed
SINGLE|ANYONECANPAY, taker completes) settles and the identity transfers with
revocation/recovery authorities preserved.
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