Skip to content

Add CLEAR_SIGN transact signing (connector toggle, dual-tx, output decoder) - #29

Merged
zy0n merged 10 commits into
mainfrom
feat/clear-sign-transact
Jul 25, 2026
Merged

Add CLEAR_SIGN transact signing (connector toggle, dual-tx, output decoder)#29
zy0n merged 10 commits into
mainfrom
feat/clear-sign-transact

Conversation

@zy0n

@zy0n zy0n commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Grows the CLEAR_SIGN transact surface from the 0.3.0 builders into full signing.
Requires firmware 1.6.1 (clear-sign-v1) on the device. Experimental; verified
against device-measured response layouts — a live on-hardware round-trip is still
pending.

What's in it

  • Single-tx CLEAR_SIGN signing. A session orchestrator streams the full sequence
    (CS_INIT → NULLIFIER×n → BP_FIELDS → OUT_* → FINALIZE), collects each output
    response, and parses the 129-byte finalize (signature + message hash). Exposed
    through the signer and the controller.
  • Clear-sign toggle on the connector sign flow. sign takes an optional
    plaintext transact; when supplied, the device reviews the recipients, tokens, and
    amounts and generates the output ciphertexts, and the result carries the device
    message hash and per-output responses alongside the signature. Without it, sign
    blind-signs exactly as before — a pure addition; the structural-identity locks are
    unchanged.
  • Dual-tx (txToken ≠ feeToken) clear-sign via signClearMultiTransact — one
    signature per sub-transaction, capped at the device maximum of two.
  • decodeClearSignOutput decodes a raw OUT_* response into its structured
    fields (random, sender/recipient blinding keys, IV, tag, ciphertext, senderRandom,
    and the transfer annotation IV).
  • 0xb007 (RAILGUN_CLEAR_SIGN_STATE) maps to a clear, best-effort error.

Testing

547 unit tests pass (MockTransport with device-measured fixtures); no type errors;
frozen structural-identity locks intact. CHANGELOG updated for 0.4.0; the release is
cut separately from main via yarn release:minor after merge.

zy0n added 10 commits July 24, 2026 19:27
RailgunSigner.signClearSignTransact drives the full clear-sign session end to
end — CS_INIT → NULLIFIER×n → BP_FIELDS → OUT_*×m → FINALIZE — validating the
shape up front, streaming the builders in order, length-checking and collecting
each output's opaque device response, and parsing the FINALIZE signature +
echoed message hash. Turns the pure builders into a usable "sign a transact"
call; the raw output responses are returned for the caller to splice into the
on-chain transact calldata.

Adds buildClearSignOutput (tagged-output dispatcher), the transact request/
result types, parseClearSignOutputResponse, and the device-measured response
lengths (OUT tuple 223B, transfer 239B, unshield 32B). Locked with a test that
reproduces the device-verified 1×1 unshield golden sequence plus a 2×3 case.
RailgunSigner.signClearSignMultiTransact clear-signs a bundle of sub-transactions
(the txToken != feeToken case = value transfer in token A + broadcaster fee in
token B) in one session: multi-tx CS_INIT, then each sub-tx's NULLIFIER×n ->
BP_FIELDS -> OUT_*×m in order, then one FINALIZE returning a signature per tx
(all under the same key).

Adds buildClearSignInitMultiTx (account ‖ nTx ‖ walletSource(15) ‖ per-tx
{merkleRoot,nIn,nOut}) and parseClearSignFinalizeMulti (256B -> two R8x‖R8y‖S‖
msgHash quads, no 0x60 prefix). Locked against the device-verified 88B multi-init
and 256B dual-FINALIZE vectors, plus a full two-tx session ordering test.
Add signClearSignTransact and signClearSignMultiTransact to LedgerController,
delegating to a RailgunSigner on the serialized device queue behind the app-
readiness gate (same pattern as the 7702 methods). This keeps the engine-facing
HardwareConnector / EngineLedgerConnector structural locks untouched — clear-sign
is a RAILGUN-app signing flow, so it lives on the controller.

Also correct the buildGetViewingKey doc: device testing shows INS 0x13 ignores
P1 (returns the key for both 0x00 and 0x01), so the prior "P1=0x01 →
SW_WRONG_P1P2" claim was wrong. We still send 0x00.
- Pre-build the entire session before sending any APDU, so field-width/range
  errors (nullifier length, bound-params, tokenHash/0zk/memo, minGasPrice) throw
  before a session is opened on the stateful device — not mid-session.
- Cap multi-tx CS_INIT at nTx <= 2 (device CS_MAX_TXS), so >2 txs fail host-side
  before any APDU rather than at the device.
- Tests: pre-flight rejection sends no APDU (single + dual), OUT_* wrong-length
  response is rejected, and >2 transactions are rejected.
Extend the connector sign fn with an optional plaintext transact (4th arg):
when provided, the device clear-signs it (reviewing recipients/tokens/amounts)
and the result carries the device outputs; otherwise it blind-signs as before.
The return type is Signature extended with an optional { msgHash, outputs }, so
existing blind callers are unaffected, and the RAILGUN engine can opt into
clear-signing by passing the transact and reading the outputs for calldata.

Threaded through both the raw HardwareConnector and the engine adapter (via the
controller's signClearSignTransact). No frozen _Reference lock change: both the
interface and the locks reference the shared sign-fn alias, so it updates in
lockstep.
…layout)

decodeClearSignOutput turns a raw OUT_* device response into structured fields
using the byte layout the firmware author's reference (clear-sign-apdus.js)
documents: tuple(208) = random(16) ‖ Blind1(32) ‖ Blind2(32) ‖ IV(16) ‖ tag(16)
‖ ciphertext(96), + senderRandom(15) [+ annotationIv(16) for transfer]; unshield
= commitment(32). These are the fields the RAILGUN engine assembles into the
on-chain transact calldata (that assembly is protocol/ABI-specific — engine side).
The clear-sign toggle on sign() covers single-tx (one signature); the
txToken != feeToken case returns one signature per tx, which doesn't fit that
return. Add signClearMultiTransact to the connector interface (raw + engine
adapter), delegating to the signer/controller on the serialized queue. Deliberate
frozen-lock update: the method is added to CommonConnectorBase and to all three
_Reference locks (HardwareConnector, EngineLedgerConnector, Legacy).
@zy0n
zy0n merged commit d9dcbfd into main Jul 25, 2026
3 checks passed
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.

1 participant