Summary
EthTx (vote-via-transaction) commits can be permanently orphaned when the UI's single registerTransaction POST to mana fails. The L1 StarknetCommit.commit() and the L1→L2 message consumption can both succeed on-chain, yet the L2 vote is only relayed if that one fire-and-forget POST reaches mana. There is no retry and no server-side fallback, so a lost POST means the user has paid L1 gas, the commit is registered on-chain, but the vote never lands and there is no recovery path.
Flow
- UI builds the Vote envelope and calls
EthereumTx.initializeVote → StarknetCommit.commit(authenticator, hash) on L1. The commit hash is poseidonHashMany of the vote calldata (packages/sx.js/src/clients/starknet/ethereum-tx/index.ts getVoteHash).
- UI then fires a single
registerTransaction POST to mana with { type, sender, hash, payload: envelope.data }:
apps/ui/src/composables/useActions.ts:81-87 (handleCommitEnvelope)
- →
apps/ui/src/helpers/mana.ts registerTransaction / rpcCall (plain fetch, no retry, result not awaited for correctness)
- →
apps/mana/src/stark/rpc.ts registerTransaction → apps/mana/src/db.ts inserts a registered_transactions row.
- The L1→L2 message is consumed on L2; the EthTx authenticator records
_commits[hash][sender] = 1.
apps/mana/src/stark/registered.ts registeredTransactionsLoop (every 15s) reads unprocessed registered_transactions rows, checks _commits[hash][sender] on-chain, and calls client.vote(...) to authenticate the vote on L2.
The critical fragility: step 4 depends entirely on the row written in step 2. The on-chain _commits entry from step 3 is never independently watched by mana. If the step-2 POST is lost (CORS, transient network error, tab closed before/while the request is in flight, browser navigation), no row is ever created, the loop never sees it, and the consumed commit sits on-chain forever with no vote.
Real occurrence (repro)
- Space:
sn:0x05702362b68a350c1cae8f2a529d74fdbb502369ddcebfadac7e91da37636947 (TEST space), proposal 65 (active)
- Voter:
0xd410007411572127c77b4c3ff88696865a589a2b
- EthTx authenticator:
0x63c89d1c6b938b68e88db2719cf2546a121c23642974c268515238b442b0ea0
- L1 commit tx:
0x28454598…f13de75
- Commit hash on-chain:
0x6591768ebc4c486cb78f00e48a7ad64cd0bca5ad6eea6f4018410cf5eb0c41c
- On L2 the EthTx authenticator records
_commits[0x6591768…c41c][0xd410…89a2b] = 0x1 (message consumed).
- mana has no
registered_transactions row for this hash (confirmed via the getDataByMessageHash RPC returning empty), and the proposal shows zero votes. The user paid L1 gas; the vote never landed and there is no way for them to recover it without re-committing.
Why recovery is hard today
The relay payload (envelope.data) is not derivable purely from on-chain state. The commit hash binds the voter's choice and the metadataUri (the optional vote "reason", pinned to IPFS). When the row is lost, that exact data — needed to reproduce the commit hash and pass the relayer's _commits check — is gone unless the voter re-supplies it. This makes the orphaned-commit case effectively unrecoverable after the fact.
Proposed fixes
- (a) Server-side self-registration (preferred): mana independently watches the
StarknetCommit CommitAdded event (or the authenticator _commits storage) and reconciles/relays known commits without depending on the UI POST. This removes the single point of failure entirely. (Note: this still needs envelope.data to build the authenticate_vote call, so it should be paired with persisting the payload — e.g. UI uploads envelope.data to a durable store keyed by commit hash before/at commit time, or mana derives what it can and surfaces unmatched on-chain commits for recovery.)
- (b) UI persistence + retry: the UI persists the pending
registerTransaction payload (localStorage/IndexedDB) immediately after the L1 commit is broadcast, and retries the POST with backoff until mana acks, surviving reloads and transient failures.
Reference: PR #2183 (vote-via-transaction).
/cc relay reliability — tagging as a bug.
Summary
EthTx (vote-via-transaction) commits can be permanently orphaned when the UI's single
registerTransactionPOST to mana fails. The L1StarknetCommit.commit()and the L1→L2 message consumption can both succeed on-chain, yet the L2 vote is only relayed if that one fire-and-forget POST reaches mana. There is no retry and no server-side fallback, so a lost POST means the user has paid L1 gas, the commit is registered on-chain, but the vote never lands and there is no recovery path.Flow
EthereumTx.initializeVote→StarknetCommit.commit(authenticator, hash)on L1. The commit hash isposeidonHashManyof the vote calldata (packages/sx.js/src/clients/starknet/ethereum-tx/index.tsgetVoteHash).registerTransactionPOST to mana with{ type, sender, hash, payload: envelope.data }:apps/ui/src/composables/useActions.ts:81-87(handleCommitEnvelope)apps/ui/src/helpers/mana.tsregisterTransaction/rpcCall(plainfetch, no retry, result not awaited for correctness)apps/mana/src/stark/rpc.tsregisterTransaction→apps/mana/src/db.tsinserts aregistered_transactionsrow._commits[hash][sender] = 1.apps/mana/src/stark/registered.tsregisteredTransactionsLoop(every 15s) reads unprocessedregistered_transactionsrows, checks_commits[hash][sender]on-chain, and callsclient.vote(...)to authenticate the vote on L2.The critical fragility: step 4 depends entirely on the row written in step 2. The on-chain
_commitsentry from step 3 is never independently watched by mana. If the step-2 POST is lost (CORS, transient network error, tab closed before/while the request is in flight, browser navigation), no row is ever created, the loop never sees it, and the consumed commit sits on-chain forever with no vote.Real occurrence (repro)
sn:0x05702362b68a350c1cae8f2a529d74fdbb502369ddcebfadac7e91da37636947(TEST space), proposal 65 (active)0xd410007411572127c77b4c3ff88696865a589a2b0x63c89d1c6b938b68e88db2719cf2546a121c23642974c268515238b442b0ea00x28454598…f13de750x6591768ebc4c486cb78f00e48a7ad64cd0bca5ad6eea6f4018410cf5eb0c41c_commits[0x6591768…c41c][0xd410…89a2b] = 0x1(message consumed).registered_transactionsrow for this hash (confirmed via thegetDataByMessageHashRPC returning empty), and the proposal shows zero votes. The user paid L1 gas; the vote never landed and there is no way for them to recover it without re-committing.Why recovery is hard today
The relay payload (
envelope.data) is not derivable purely from on-chain state. The commit hash binds the voter'schoiceand themetadataUri(the optional vote "reason", pinned to IPFS). When the row is lost, that exact data — needed to reproduce the commit hash and pass the relayer's_commitscheck — is gone unless the voter re-supplies it. This makes the orphaned-commit case effectively unrecoverable after the fact.Proposed fixes
StarknetCommitCommitAddedevent (or the authenticator_commitsstorage) and reconciles/relays known commits without depending on the UI POST. This removes the single point of failure entirely. (Note: this still needsenvelope.datato build the authenticate_vote call, so it should be paired with persisting the payload — e.g. UI uploadsenvelope.datato a durable store keyed by commit hash before/at commit time, or mana derives what it can and surfaces unmatched on-chain commits for recovery.)registerTransactionpayload (localStorage/IndexedDB) immediately after the L1 commit is broadcast, and retries the POST with backoff until mana acks, surviving reloads and transient failures.Reference: PR #2183 (vote-via-transaction).
/cc relay reliability — tagging as a bug.