Skip to content

feat!: chain_id, String amounts, Burn builder and chainInfo for the treasury chain - #3

Merged
MehranMazhar merged 2 commits into
mainfrom
treasury-break
Jul 30, 2026
Merged

feat!: chain_id, String amounts, Burn builder and chainInfo for the treasury chain#3
MehranMazhar merged 2 commits into
mainfrom
treasury-break

Conversation

@MehranMazhar

Copy link
Copy Markdown
Member

Companion to clutchprotocol/clutch-node#9. Merge together — this API cannot talk to the old node, and the old API cannot talk to the new node.

Signing format (the part that must be byte-exact)

The node moved its hash preimage to the 4-item RLP list [from (no 0x), nonce, chain_id, data] and its wire format to the 8-item list [from, nonce, chain_id, r, s, v, hash, data], with chain_id at index 2.

Two consequences here:

  1. Every createUnsigned* blob now carries chain_id{from, nonce, chain_id, data}. Clients sign from this blob, so a missing chain_id means every signature is computed over the wrong preimage and the node rejects all of them.
  2. The faucet signs the new layout. Field order is chain_id third in both lists, matching the node exactly. Pinned by a new test that RLP-decodes the faucet's own output and asserts 8 items with chain_id at index 2 — the only guard against silently drifting from the node's format, since nothing else here would notice.

Why chain_id at all: a signature previously committed to a transaction's contents but not to its network, so once Mint exists a testnet mint could be replayed verbatim on mainnet and still verify.

Fares become String, not Int

GraphQL's Int is 32-bit. At the peg of 1 USD = 1,000,000 CLT, i32 overflows at a fare of about $2,147 — an ordinary ride price. Every fare/amount/balance scalar is now a decimal String, parsed through one helper that rejects non-integer, negative, and above-i64::MAX values.

Note the asymmetry, which is deliberate: the node still sends fare/fare_paid as bare JSON numbers — only total_supply moved to a string there, because it is the one value that can pass 2^53. The conversion therefore happens at this GraphQL boundary, so SDK clients should expect String from the hub regardless of what the node emits.

New surface

  • createUnsignedBurn(amount, redemptionRef) — the redemption half of the treasury. A user burns CLT carrying the hash of an off-chain redemption intent, and the treasury's watcher matches the confirmed burn to that intent and pays out.
  • chainInfochainId, isTestnet, txFee, totalSupply, mintAuthority. Fetched once at startup and held in schema data, since these are genesis constants; a node swap needs an API restart.

Two security changes

  • The faucet refuses to boot on a non-testnet chain. If faucet_enabled and is_testnet is false, the process panics at startup. A faucet surviving onto a real network would destroy the peg on day one, and this is the one place the codebase panics rather than returning Result — boot-time fatal misconfiguration.
  • The auth challenge is chain-bound: clutch-auth:{chain_id}:{publicKey}:{timestamp}. It previously carried no chain or hub identity, so a challenge signed on testnet authenticated the same key against any other Clutch hub inside the ±120s window — and money endpoints are about to be gated on these JWTs. Breaking, no fallback; the cross-language fixtures were regenerated.

Verification

cargo test25/25 pass, no new warnings. Includes the new faucet wire-format test and the regenerated auth fixtures.

🤖 Generated with Claude Code

…reasury chain

The node's breaking release moved the hash preimage to
[from, nonce, chain_id, data] and the wire format to an 8-item list with
chain_id at index 2, so every unsigned blob now carries chain_id and the
faucet signs the new layout (pinned by a test that decodes its own output).

Fares, amounts and balances move from GraphQL Int to String: Int is 32-bit
and at 1 USD = 1,000,000 CLT it overflows at a ~$2,147 fare. Adds
createUnsignedBurn and a chainInfo query (totalSupply as String - the one
node field that can pass 2^53). The faucet refuses to boot on a
non-testnet chain, and the auth challenge is now chain-bound so a captured
testnet signature cannot authenticate against another network.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
faucet_amount_clt = 1000 was meaningful when CLT was a whole unit. At
1 USD = 1,000,000 CLT it is $0.001 - exactly one tx_fee - so a funded
test account could send a single zero-value transaction and reach zero
without ever affording a ride. Verified against a live stack: the drip
credited 1000 while the same transaction's fee was also 1000.

Rate limits (per-IP 30s, per-address 1h) bound the larger drip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MehranMazhar
MehranMazhar merged commit 6cf1f75 into main Jul 30, 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