[codex] Add client protocol reference helpers#43
Conversation
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 88f64a2b67
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const burn = tx.burn ?? 0; | ||
| debit(st, tx.from, tx.amount + tx.fee + burn); | ||
| if (burn > 0) credit(st, NULL_ADDRESS, burn); | ||
| st.nonces.set(tx.from, (st.nonces.get(tx.from) ?? 0) + 1); | ||
| credit(st, tx.to, tx.amount); |
There was a problem hiding this comment.
Match client replay to consensus escrow payouts
For chains that include red-packet or staking transactions, this transfer-only accounting diverges from the consensus state machine in Blockchain.applyTx: CLAIM|, REFUND|, and UNSTAKE| have amount === 0 but credit payouts from escrow, and SLASH| moves stake to the treasury, while this helper only deducts the sender fee and credits tx.to by zero. Any wallet/indexer using the new replayClientState()/clientBalanceOf() helpers on full blocks will show incorrect balances after those already-supported transaction types appear.
Useful? React with 👍 / 👎.
| if (window.maxBlocks !== undefined && blocks.length > window.maxBlocks) { | ||
| return { ok: false, error: `recent blocks 超过窗口上限 ${window.maxBlocks}` }; | ||
| } | ||
| for (const block of blocks) { | ||
| if (window.minTimestamp !== undefined && block.timestamp < window.minTimestamp) { | ||
| return { ok: false, error: `#${block.index} 早于 recent minTimestamp` }; | ||
| } | ||
| const checked = verifyBlockAgainstHeaders(block, headers); | ||
| if (!checked.ok) return { ok: false, error: checked.error }; | ||
| } | ||
| return { ok: true, value: blocks }; |
There was a problem hiding this comment.
Require the complete recent-block suffix
When verifying a QUERY_RECENT snapshot, this accepts any subset of eligible blocks: an empty recentBlocks array, or one with a recent block omitted, passes as long as the returned blocks are not too many and not too old. Because the headers already reveal the tip height and timestamps, this should require the exact suffix headers.slice(-maxBlocks).filter(h => h.timestamp >= minTimestamp) to be present; otherwise a node can omit a recent block containing the wallet's transaction and the client will treat the stale snapshot as verified.
Useful? React with 👍 / 👎.
| const checked = verifyHeaderChain(headers); | ||
| if (!checked.ok || checked.work === undefined) return { ok: false, error: checked.error ?? 'header 链校验失败' }; |
There was a problem hiding this comment.
Enforce consensus difficulty for headers
For light clients this delegates to verifyHeaderChain(), which only checks that each hash satisfies the difficulty value embedded in that header; it never recomputes the consensus expectedDifficulty from the prior headers. After the last checkpoint, a peer can mine a fork with artificially lowered difficulty (even 0), pass verifyClientHeaders(), and then serve recent/proof data from a chain full nodes would reject. The client protocol verifier needs to reject headers whose difficulty is not the consensus value before trusting the tip/work.
Useful? React with 👍 / 👎.
| for (const tx of block.transactions) { | ||
| if (!verifyTransaction(tx)) return { ok: false, error: `#${block.index} 交易 ${tx.txid.slice(0, 12)} 自洽性失败` }; |
There was a problem hiding this comment.
Reject non-consensus coinbase layouts
Recent blocks are accepted after only per-transaction self-checks, but verifyTransaction() allows any coinbase with a positive amount and does not enforce that there is exactly one coinbase in position 0 or that its amount is BLOCK_REWARD + fees. A peer that can mine an otherwise valid-looking header can include extra or overpaying coinbase transactions, this helper will accept the block, and replayClientState() will credit fabricated rewards that full nodes reject in Blockchain.validateChain().
Useful? React with 👍 / 👎.
| const header = headers.find((h) => h.index === block.index); | ||
| if (!header) return { ok: false, error: `缺少 #${block.index} header` }; | ||
| if (header.hash !== block.hash) return { ok: false, error: `#${block.index} hash 不在 header 链中` }; | ||
| if (blockHeader(block).merkleRoot !== header.merkleRoot) return { ok: false, error: `#${block.index} merkleRoot 与 header 不一致` }; |
There was a problem hiding this comment.
This only compares the returned block's Merkle root to the trusted header, leaving the other header fields in the full block object unchecked. Because verifyRecentBlocks() tests block.timestamp before calling this, a peer can take an older block, change its timestamp into the requested window while keeping hash and merkleRoot copied from the real header, and still pass this verifier. Compare the entire blockHeader(block) to the matched header, or recompute the hash from the full block header, before accepting it.
Useful? React with 👍 / 👎.
| const checked = verifyHeaderChain(headers); | ||
| if (!checked.ok || checked.work === undefined) return { ok: false, error: checked.error ?? 'header 链校验失败' }; |
There was a problem hiding this comment.
Reject headers beyond the local future-drift limit
This header verifier omits the consensus timestamp guard from Blockchain.validateChain() that rejects blocks with timestamp > Date.now() + MAX_FUTURE_DRIFT_MS. A peer can therefore serve a header chain containing far-future blocks that full nodes would reject, and the light client will still trust that tip and any recent blocks/proofs anchored to it. Apply the same future-drift check while walking headers before returning ok.
Useful? React with 👍 / 👎.
| for (const tx of block.transactions) { | ||
| if (!verifyTransaction(tx)) return { ok: false, error: `#${block.index} 交易 ${tx.txid.slice(0, 12)} 自洽性失败` }; |
There was a problem hiding this comment.
Reject non-coinbase NULL_ADDRESS outputs
verifyTransaction() intentionally only checks transaction self-consistency, so a signed non-coinbase transfer with to === NULL_ADDRESS passes here even though full nodes reject it in Blockchain.validateChain(). If such a transaction appears in a served recent block, this verifier accepts the block and replayClientState() treats the amount as credited to the null address outside the burn path, producing state from a chain the network will not accept.
Useful? React with 👍 / 👎.
| } else if (!verifyTxInclusionProof(proof)) { | ||
| return { ok: false, error: `交易 ${proof.tx.txid.slice(0, 12)} Merkle proof 无效` }; | ||
| } else if (!verifyTransaction(proof.tx)) { | ||
| return { ok: false, error: `交易 ${proof.tx.txid.slice(0, 12)} 自洽性失败` }; |
There was a problem hiding this comment.
Require headers before trusting address proofs
When callers omit headers, this path accepts a Merkle proof against the proof's self-supplied block header; verifyTxInclusionProof() does not check PoW, checkpoints, or whether that header is on the verified chain. A wallet that replays ADDRESS_PROOFS without separately passing the header chain can be given fabricated proofs and an arbitrary balance. Require verified headers here, or return an explicitly untrusted result when they are absent.
Useful? React with 👍 / 👎.
What changed
packages/core/src/client-protocol.tsas the reusable CLIENT-PROTOCOL reference layer for light clients and indexers.scripts/client-protocol-test.tscovering CLIENT-PROTOCOL §9 vectors for address derivation, preimage bytes, txid, ed25519 signatures, shared key, fixed-nonceENC|memo, and decrypt.docs/blockchain/CLIENT-PROTOCOL.md.Why
Wallets and indexers should be able to keep only recent full blocks, verify the header chain, and lazily request older address or transaction proofs without becoming full nodes. This does not change consensus; it gives clients a small reference implementation to follow or port.
Validation
corepack pnpm exec tsx scripts/client-protocol-test.tscorepack pnpm exec tsx scripts/light-sync-test.tscorepack pnpm typecheckcorepack pnpm exec tsx scripts/integration.tscorepack pnpm smokeNote: local commands still print the existing
/Users/v0id/.npmrcpermission warning, but all checks pass.