fix: two concurrent dApp transactions are populated with the same nonce, and the second fails terminally #271
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
#269 moves transaction population into the background, before the approval window opens, so that the signed artifact can be verified against the object the user was actually shown. That is the right structure and it closed a real gap — but it moves when the nonce is chosen.
Previously the popup called
populateTransaction()on Confirm, so a second transaction picked up a fresh nonce at the moment the user confirmed it. Now both are populated before either window opens. Two concurrenteth_sendTransactioncalls — or one approval window left open while another transaction goes out — get the same nonce, and the second broadcast fails terminally with the "may still have reached the network" wording.Found by the independent review of that PR. It fails CLOSED and is inherent to the option the issue recommended, so it was not blocked there. It is undocumented, and the wording the user sees is wrong for this case: the second transaction did not reach the network and never will, but the copy tells them it might have.
Implementation requirements
Definition of done
eth_sendTransactionrequests behave as decided, not by accident of population order.TODO.mdupdated in the same commit.make checkpasses.Plan: option three — refuse a second transaction approval while one is pending.
Why not the other two. Re-populating at Confirm reintroduces exactly the gap #216 closed: the verified nonce would no longer be the displayed one. Allocating around in-flight approvals makes the wallet's own bookkeeping the authority on a nonce the network has not accepted, and an approval the user abandons then leaves a hole that stalls every later transaction until the worker restarts. Refusing keeps the displayed object the verified object, holds no state the network can contradict, and fails the second request while the page is still waiting and nothing has been shown.
Mechanism:
eth_sendTransactionbranch before its firstawaitand released when the request is answered (approved and broadcast, rejected, or window closed). The second request is refused with EIP-1193 code-32002and never reachesprepareApprovalTx(), so no second nonce is allocated. Sign approvals are unaffected — they consume no nonce.nonce too low,replacement transaction underpriced, ethersNONCE_EXPIRED/REPLACEMENT_UNDERPRICED) gets its own stage and its own sentences — it did not reach the network, send it again.already knowndeliberately keeps the existing wording: a node saying it has the transaction means it did reach the network.Proof: tests over two overlapping
eth_sendTransactioncalls (asserting the node is asked for a nonce once, one window opens, the first still settles, the slot frees afterwards), over the local pre-broadcast refusal (assertingbroadcastTransactionis never called for the second), and over the copy each failure produces including thealready knowndistinction. Each new test demonstrated failing against the unfixed code first, with that output in the PR body.