Skip to content

contracts: sign a contract spend only as the wallet showed it - #36

Merged
GracedEternalKingCabbageMan merged 1 commit into
sequentiafrom
feature/five-point-signing
Oct 8, 2026
Merged

GracedEternalKingCabbageMan merged 1 commit into
sequentiafrom
feature/five-point-signing

Conversation

@GracedEternalKingCabbageMan

Copy link
Copy Markdown
Collaborator

Signing for a contract under the five-point rule. A wallet signs a contract spend only through Approval, which checks the rule, shows what is signed, and signs only the digest of what it showed.

What it does

  • Approval::prepare(spend, view, signer) checks:

    1. the template hash is on the wallet's list;
    2. the output was recomputed and the coin pays it;
    3. the program was run against the final transaction;
    4. writes the summary of point 4;
    5. the key is a contract key under m/8383h/{coin}h/0h, the one the path names.

    It also checks the chain's locks.

  • The summary shows:

    • the template: by registry name, else its commitment root;
    • the path: who can take it and its effect;
    • every parameter by role: ticker, amount in the asset's precision, the wallet's key marked, a relative lock as a delay;
    • the wallet's balance change in every asset, the contract's, the payments and the fee;
    • each check in words, and a digest over the whole summary.
  • Approval::sign(shown_digest, signer) refuses any other digest. It prepares the spend again, refuses if anything differs, and runs the program once more against the transaction it returns. A signature made only to run the program before approval is dropped inside the engine.

  • Spend::finalize becomes crate-private, so the gate is the only way to sign.

  • lwk_wasm: ContractApproval.prepare(spend, signer, viewJson), .summary(), .digest(); Signer.signContractSpend(approval, shownDigest).

Proof (node v25.2.1, elementsregtest, Simplicity from genesis, -par=1). Before this change the new tests do not compile (unresolved import lwk_contracts::approval), and the wasm drive fails at ContractApproval undefined.

  • cargo test -p lwk_contracts --test approval, no chain needed:
    • the summary shows root c1a71ea2…46fa unregistered, or sequentia/one-key v1 (registered);
    • a digest that was not shown, another wallet, a template not on the list, a key outside the contract account, and the wrong contract key are each refused.
  • --test drip_regtest through the gate: the drip confirms; the three bad drips are refused by the engine and, forced, in blocks (bad-txns-nonfinal; Assertion failed inside jet twice).
  • lwk_wasm/tests/node/contract_drip_regtest.js, the wallet flow in wasm:
    • the approval shows sequentia/faucet-drip v1 (registered), all 13 parameters by role, and the wallet's change 500.00000000 tSEQ;
    • drip 1 confirms at 581 vB, exactly the vsize shown;
    • a digest that was not shown, another wallet, a template not on the list and a non-contract key are each refused;
    • the early drip is refused by the engine; signed by an engine told the interval had passed, it is refused by the mempool (non-BIP68-final) and in a block (bad-txns-nonfinal);
    • the over-tier drip and the wrong successor are each refused by the engine, naming the failing check;
    • drip 2 confirms.

The engine could sign any spend it built. A wallet must sign for a
contract only when five things hold: the template is on its list, it
recomputed the output, it ran the program against the final transaction,
it showed the template, path, parameters by role and its own balance
change, and the key is one reserved for contracts.

Approval::prepare checks the first, second, third and fifth, and the
chain's locks, and writes the summary the wallet shows (the template by
the registry's name, else its commitment root; every parameter by role;
the wallet's balance change in every asset beside the contract's, the
payments and the fee) with a digest over it. Approval::sign signs only
when handed that digest back, after preparing the spend again and
running the program once more against the transaction it returns.
Spend::finalize is no longer public, so the gate is the only way in.

lwk_wasm exposes ContractApproval and Signer.signContractSpend, so a
browser wallet shows the summary and signs what it showed.
@GracedEternalKingCabbageMan
GracedEternalKingCabbageMan merged commit 99e6d0e into sequentia Oct 8, 2026
12 of 18 checks passed
@GracedEternalKingCabbageMan
GracedEternalKingCabbageMan deleted the feature/five-point-signing branch October 8, 2026 02:51
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