Pin nonce+EIP-1559 fees at send; surface pre-sign hash (#37) - #39
Merged
Conversation
Server now pins nonce, maxFeePerGas, maxPriorityFeePerGas, and gasLimit at send time, forwards them via WalletConnect's eth_sendTransaction, and computes the EIP-1559 pre-sign RLP hash Ledger displays in blind-sign mode. A new LEDGER BLIND-SIGN HASH content block is emitted on every EVM send for verbatim user relay — restoring on-device calldata integrity that the PR #35 hash-drop left unchecked. Explicit in-block warning covers Ledger Live's Edit-gas path; RPC failure throws rather than silently falling back to unpinned. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e txHash Live-test follow-ups to the EIP-1559 pre-sign hash work: - A single MCP tool call cannot emit content while the Ledger device prompt blocks — the hash block appeared AFTER the user approved on-device. Split the flow: `preview_send(handle)` pins nonce + fees, computes the pre-sign hash, stashes both on the handle, and emits the LEDGER BLIND-SIGN HASH block. `send_transaction` reads the stash and submits; throws "Missing pinned gas" if preview_send was skipped. TRON is unaffected. - Fee pin was too conservative (viem's 1.2× baseFee + node priority estimate left a live-test tx stuck in the mempool). Bumped to `baseFee × 2 + max(estimateMaxPriorityFeePerGas, 1.5 gwei)`. - Agent sometimes dropped the txHash from chat after a successful broadcast, forcing the user to dig through Ledger Live. Added a verbatim-relay TRANSACTION BROADCAST block with the hash + explorer link, emitted before the polling directive. - Edit-gas paragraph in the hash block now phrases the tradeoff as the user's choice (accept divergence without the hash-match guarantee, or reject and re-preview) rather than a flat "must reject" directive. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two layered defenses on top of the prepare/send fingerprint and Ledger
device display, with integration tests pinning both behaviors.
PREPARE RECEIPT: every prepare_* now emits a verbatim-relay block
listing the raw args the agent supplied. Raises the tampering bar for
narrow agent compromise (prompt injection from other tool output,
malicious skills, compromised subagents) — the agent's bullet summary
is free-composed prose; the receipt is receipts. Threaded through
handler({toolName}) so all 17 EVM + 7 TRON prepare tools emit it.
Agent-side hash recompute: preview_send now attaches an agent-task
block instructing the agent to OFFER the user a fourth trust-boundary
option — independently keccak(serializeTransaction(tuple)) with viem
and compare against MCP's reported preSignHash. Catches a compromised
MCP that lies about the hash. Per-call values are spliced into the
command template so the check is cheap to run.
README has a reworked "Security model" section with a threat→defense
table, honest limits, and the agent/MCP asymmetry. Integration tests
cover both the happy path and tampered cases.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
nonce,maxFeePerGas,maxPriorityFeePerGas, andgasLimitserver-side at SEND time (not prepare — gas moves), forwards them through WalletConnect'seth_sendTransaction, and computes the EIP-1559 pre-sign RLP hash that Ledger displays in blind-sign mode.LEDGER BLIND-SIGN HASH — RELAY VERBATIM TO USER; THEY MATCH ON-DEVICEcontent block on every EVMsend_transaction, with an explicit load-bearing warning that tapping "Edit gas" / "Edit fees" in Ledger Live legitimately desyncs the hash (reject on-device, re-run).renderAgentTaskBlock's final Ledger reminder to point at the send-time block as authoritative; updatedTRANSACTION VERIFICATIONand addedLEDGER BLIND-SIGN HASHparagraphs to the server-level instructions.Approach
eip1559PreSignHashinsrc/signing/verification.tsvia viem'sserializeTransaction({type:"eip1559", ...}) + keccak256. Golden cross-check in tests against a viem-independent re-serialize to catch shape drift.requestSendTransactioninsrc/signing/walletconnect.tsgained aPinnedGasFieldsarg; when present, all four fields are hex-encoded in the RPC params. Legacytx.gasEstimatefallback kept for any caller that doesn't pin.sendTransactioninsrc/modules/execution/index.tsfetches pendinggetTransactionCount,estimateFeesPerGas, andestimateGasin parallel just before the WC request; returns{preSignHash, pinned, to, valueWei}so the handler can emit the block without re-reading the consumed handle.sendTransactionHandlerinsrc/index.tssplices the new block between the JSON result andrenderPostSendPollBlock.Caveats & acceptance-criterion follow-up
prepare_native_send+send_transactionon mainnet/testnet to confirm it honors all four pinned fields (or document which ones it silently overrides). I can run this at your direction; if any field turns out to be overridden, we'd scope the pin down in a follow-up PR before relying on the hash match in docs.Test plan
npx tsc --noEmitnpx vitest run— 399/399 passing (addedtest/send-hash-pin.test.tsfor the golden hash, pinned-field propagation, RPC-failure-throws path; updatedtest/simulation.test.tsforward-test to assert the pin reaches WC)🤖 Generated with Claude Code