feat(forks): register nonceEnforcement fork + ship assignNonce validation infra (batch C PR 1)#884
Conversation
The 2026-05-28 bug-hunt audit flagged assignNonce as a hardcoded stub.
Investigation during scoping found the nonce gap is 3-layered: RPC
validation missing, GCREdit emission missing, consensus-time
validation missing. Cross-RPC replay through different nodes is
currently possible — two block-included txs from the same sender both
carrying tx.content.nonce = N+1 would both succeed because
GCRNonceRoutines blindly applies its add edit.
Spec captures:
- Strict sequential semantics
(tx.content.nonce === account.nonce + 1 + pending_mempool_count)
- New nonceEnforcement fork, gated like gasFeeSeparation
- GCREdit nonce variant gains optional `expectedPrior: number` for
consensus-time check
- 3-PR sequence (validation infra, mempool lookahead,
consensus rule + fork activation)
- Single SDK publish before PR 3 lands; PRs 1+2 need no SDK touches
- Devnet activates fork at block 0 to keep the existing
wipe_and_reboot.sh / docker --clean flow working unchanged
This commit is the design lock-in before any code lands. PR 1 ships
next with the validation infra behind the (still commented-out)
caller.
…tion infra (batch C PR 1)
PR 1 of the 3-PR batch C sequence specified in
`docs/specs/audit-sweep-batch-c-nonce.md`. Lands the validation infra
behind the (still commented-out) caller in confirmTransaction — no
live behaviour change in this PR.
Fork registration:
- New `nonceEnforcement` ForkName + NonceEnforcementConfig (alias of
BaseForkConfig; no payload beyond activationHeight + description).
- Default registry entry with activationHeight: null so existing
chains stay bit-identical.
- writeForkConfig + validateForkEntry switch branches updated; the
exhaustiveness `never` guard catches future ForkName additions
at compile time.
- isForkActive is generic, picks up the new name automatically.
assignNonce real implementation (validateTransaction.ts):
- Pre-fork: always returns true. Bit-identical to the previous
hardcoded stub. Re-syncing pre-fork blocks is unaffected.
- Post-fork: strict equality check
`tx.content.nonce === account.nonce + 1`
on the sender's GCR account.
- Read-only — no GCR mutation. Increment is emitted as a +1 nonce
GCREdit by HandleNativeOperations.handle() in PR 3, applied at
consensus time by GCRNonceRoutines.
- Caller in confirmTransaction (lines 77-86) remains commented out
in PR 1. PR 3 uncomments it once the consensus-side rejection
(GCREdit expectedPrior) is in place, so validation and the
apply-time check ship together behind the same fork gate.
Devnet:
- testing/devnet/genesis.devnet.json gains a `nonceEnforcement`
fork entry with activationHeight: 0 so fresh devnet docker boots
already post-fork. Matches the existing osDenomination pattern;
`wipe_and_reboot.sh` / docker --clean flow needs no changes.
Out of scope (per design doc):
- PR 2: mempool-aware pending-count lookahead in assignNonce so
back-to-back submissions from one sender are accepted in order.
- PR 3: GCREdit `expectedPrior` field, nonce-edit emission in
HandleNativeOperations, consensus-side rejection in
GCRNonceRoutines, caller uncomment. Requires SDK type update.
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
|
Warning Review limit reached
More reviews will be available in 52 minutes and 25 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughThis PR implements fork-gated sequential nonce validation to prevent transaction replay. It adds a new ChangesNonce-based Replay Protection (nonceEnforcement Fork)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR registers the
Confidence Score: 5/5Safe to merge — the live transaction path is unchanged because the assignNonce caller remains commented out, and all new code paths are behind a fork gate that defaults to inactive on non-devnet chains. All four changed source files are internally consistent. The assignNonce implementation correctly addresses phantom-account creation and attacker-controlled height selection from prior review rounds. The fork wiring follows the established pattern and exhaustiveness guards will catch any future ForkName additions at compile time. No files require special attention before merge. The height-source note in validateTransaction.ts is worth revisiting when PR 3 uncomments the assignNonce caller. Important Files Changed
Sequence DiagramsequenceDiagram
participant RPC as RPC Ingress
participant CT as confirmTransaction
participant AN as assignNonce (PR 1 — dormant)
participant GCR as GCRMain (DB)
participant FG as isForkActive
participant CSS as Consensus (PR 3)
RPC->>CT: submit tx
CT->>CT: Chain.getLastBlockNumber() → referenceBlock
note over CT,AN: Nonce caller commented out in PR 1
CT--xAN: (commented) assignNonce(tx)
AN->>FG: isForkActive(nonceEnforcement, lastBlockNumber)
alt pre-fork
FG-->>AN: false → return true
else post-fork
AN->>GCR: findOne pubkey senderAddress
GCR-->>AN: account or null
AN->>AN: "check tx.nonce === account.nonce + 1"
AN-->>CT: true / false
end
CT->>FG: isForkActive(gasFeeSeparation, referenceBlock)
CT->>CT: signature verify, runTypeDispatcher
CT-->>RPC: ValidityData signed
RPC->>CSS: broadcast tx
CSS->>CSS: PR 3 GCREdit.expectedPrior check at apply-time
Reviews (2): Last reviewed commit: "fix(audit-sweep): address Greptile + Cod..." | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/specs/audit-sweep-batch-c-nonce.md`:
- Around line 74-77: The fenced code block containing the line "incoming tx is
valid iff" and the expression "tx.content.nonce === account.nonce + 1 +
pending_count(sender_in_mempool)" should include a language identifier to
satisfy markdownlint MD040; update the opening fence from ``` to ```text (or
another appropriate language) so the block becomes "```text" and leave the block
contents unchanged.
- Around line 126-130: The PR breakdown incorrectly states that PR 3 "includes
'new fork'" while the code already registers the nonceEnforcement fork; update
the table/description to reflect that nonceEnforcement is already registered in
this PR rather than being introduced later. Edit the entry referencing PR 3 and
mention that nonceEnforcement is registered here (point to GCRNonceRoutines.ts,
HandleNativeOperations.ts emit-side changes, and validateTransaction.ts
uncommented caller) and adjust the risk/rollout note to avoid implying the fork
registration is pending. Ensure the language clarifies sequencing for reviewers
and matches the actual changes in validateTransaction.ts and related files.
In `@src/libs/blockchain/routines/validateTransaction.ts`:
- Around line 393-395: The code currently uses tx.blockNumber to compute
blockHeight (const blockHeight = tx.blockNumber ??
getSharedState.lastBlockNumber ?? 0) which allows a caller to spoof a stale
block and bypass fork-based nonce enforcement; change the logic to never trust
tx.blockNumber for fork gating—derive blockHeight solely from the node/shared
state (use getSharedState.lastBlockNumber ?? 0) and update any uses (e.g.,
isForkActive calculation) to reference that trusted value instead of
tx.blockNumber.
- Around line 402-406: The senderAddress value (computed in
validateTransaction.ts as senderAddress and using forgeToHex(tx.content.from))
must be canonicalized before any GCR/nonce lookup to avoid casing-based key
divergence; update the assignment that computes senderAddress to normalize both
string inputs and forgeToHex outputs to a consistent canonical form (e.g.,
.toLowerCase() or your project's canonicalAddress helper) so subsequent
nonce/account lookups use the normalized key.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 68bdf26a-d359-4be8-938f-aad9239a6ff3
📒 Files selected for processing (5)
docs/specs/audit-sweep-batch-c-nonce.mdsrc/forks/forkConfig.tssrc/forks/loadForkConfig.tssrc/libs/blockchain/routines/validateTransaction.tstesting/devnet/genesis.devnet.json
…eploop iter 1) Four code findings + three doc findings from the iter-1 review of PR #884. Code — validateTransaction.ts assignNonce: Critical (CodeRabbit / Greptile P2 boundary): Fork gate must not use tx.blockNumber. The caller is RPC-facing, so tx.blockNumber is attacker-controlled — a forged tx with a stale block height could select pre-fork behaviour and bypass the nonce check entirely once the caller is uncommented in PR 3. Drop the `tx.blockNumber ??` clause; gate on `getSharedState.lastBlockNumber ?? 0` only. Add a comment documenting the one-block boundary window where the gate reads inactive at ingress but active at inclusion; PR 3's consensus-side `expectedPrior` check is the safety net for that exact case. P1 (Greptile): `ensureGCRForUser` calls `HandleGCR.createAccount` for unknown pubkeys. Once the caller is uncommented in PR 3, validation runs before signature verification, so any syntactically valid `from` would provision a phantom account row as a side effect of nonce validation (DB bloat + ahead of the crypto check). Replace with a direct `gcrRepository.findOne` lookup. Unknown sender returns false (a real sender that has never transacted still has a row at `nonce === 0` from genesis seeding or a prior received tx — never having a row means they can't have a valid nonce to submit either). Major (CodeRabbit): Sender address not canonicalised before lookup. Two submissions that differ only in case would target different DB rows and weaken replay protection. Lowercase the resolved hex string before the findOne call. Cleanup: drop the unused `ensureGCRForUser` import. Replace with direct `Datasource` + `GCRMain` imports mirroring the existing pattern in `identityHandlers.ts`. Update the JSDoc to document the read-only contract and the rationale for the node-local block-height read. Docs — audit-sweep-batch-c-nonce.md: Minor (CodeRabbit): Fenced code block at line 74 missing a language tag — markdownlint MD040. Add `text`. P2 (Greptile + CodeRabbit): PR breakdown table claimed PR 1 was "Validation infra (no fork gate)", listed `HandleNativeOperations.ts` and tests as PR 1 files, and reserved "new fork" for PR 3. Reality: PR 1 already registers the fork (gated default `activationHeight: null`), touches `forkConfig.ts` / `loadForkConfig.ts` / devnet genesis, and does not touch `HandleNativeOperations.ts` or ship tests. Rewrite the row to match the actual scope. Move "new fork" language out of PR 3 — it's already done here. Bug-class scope note: the iter-1 critical (tx.blockNumber for fork gating) appeared only because of the new code path; nothing else in the codebase reads `tx.blockNumber` for fork-gating purposes (grep across `src/` confirms). PR 2 and PR 3 inherit the same node-local block-height read.
…tch C PR 3) [DRAFT - review needed] (#886) * feat(consensus): nonceEnforcement consensus rule + caller wire-up (batch C PR 3 / Path A) Final PR of the 3-PR batch C sequence specified in `docs/specs/audit-sweep-batch-c-nonce.md`. Closes the cross-RPC replay window end-to-end and activates the `nonceEnforcement` fork on devnet (already active at block 0 since PR 1's genesis fixture). Path A — SDK ships type-only, node populates `expectedPrior` at apply time. Chosen over Path B (SDK-side emit) and Path C (SDK fork introspection) for zero breaking-change risk: old SDK clients continue working post-fork without re-publishing. See spec doc §"Emission-site rationale" for the trade-off matrix. Changes: src/libs/blockchain/gcr/gcr_routines/GCRNonceRoutines.ts - When `expectedPrior` is set on a non-rollback nonce edit, reject the apply iff `accountGCR.nonce !== expectedPrior`. - Rollback path skips the check (unwinding, not validating forward progress). - Cross-RPC double-spend safety net: two competing same-nonce txs bundled into the same block from different RPCs both pass per-node validation but only one survives consensus apply. src/libs/blockchain/gcr/handleGCR.ts - In `applyTransaction`'s per-edit loop, populate `expectedPrior` on every `nonce` edit when `nonceEnforcement` is active at the apply block. Read live from `entities.accounts` so a hypothetical second nonce edit in the same tx sees the post-first-apply value. - Block height: `tx.blockNumber ?? lastBlockNumber ?? 0`. - Skip on rollback. Skip if `expectedPrior` is already set (defence against future SDK-side population). src/libs/network/endpointValidation.ts - Symmetric strip of `expectedPrior` on both `gcrEdits` (regen) and `txEditsBlanked` (SDK-shipped) before hash compare. Mirrors the existing `txhash` blanking pattern. - SDK never writes the field today; the strip is defence in depth against future SDK-side emission and any non-conformant clients. src/libs/blockchain/routines/validateTransaction.ts - Uncomment `assignNonce` caller (lines 80-89). Pre-fork it short-circuits to true (PR 1). Post-fork enforces strict sequential semantics with mempool lookahead (PR 2) + the matching consensus-side rejection above. package.json - Bump `@kynesyslabs/demosdk` from 4.0.3 to 4.0.5 — the published SDK that adds the `expectedPrior?: number` field to `GCREditNonce` (type-only, runtime unchanged). docs/specs/audit-sweep-batch-c-nonce.md - Update PRs list (#885 merged, #884 merged, #TBD = this PR). - Add `sdk_publish: 4.0.5` to frontmatter. - Rewrite §Fork activation Post-fork bullet to describe the Path A flow (node populates, not SDK emits). - Add §Emission-site rationale documenting why Path A was chosen over B / C. - Update PR 3 row in the breakdown to list actual files + summarise the SDK pin bump. Verification: - `tsc --noEmit` clean on all six changed files. - SDK 4.0.5 verified locally: `expectedPrior?: number` present in `node_modules/@kynesyslabs/demosdk/build/types/blockchain/GCREdit.d.ts`. - No new e2e test in this commit; will follow up in a separate PR with the devnet two-RPC double-broadcast scenario once the base wires are merged. Cross-RPC double-spend scenario (post-fork): 1. Client signs tx with `content.nonce = N+1`. 2. Broadcasts to RPC-A and RPC-B in parallel. 3. Both RPCs pass per-node validation (mempool lookahead is single-node). 4. Block-formation gathers both → applies in deterministic order. 5. First apply: account.nonce goes N → N+1. `expectedPrior` matched (N). 6. Second apply: `expectedPrior = N` (populated at apply time from pre-first-apply snapshot), but `accountGCR.nonce = N+1`. Mismatch → `GCRNonceRoutines` rejects → whole tx rolls back at consensus. * fix: move expectedPrior populate from apply-time to validation-time (PR #886 self-review) Caught during draft-PR review: the apply-time populate site I shipped in the initial commit was incorrect. Bug: Populating `expectedPrior` inside `HandleGCR.applyTransaction`'s per-edit loop reads `entities.accounts` which already reflects prior in-block applies. For two competing same-nonce txs in the same block: 1. Tx A applies. Edit populates `expectedPrior = N` from live state. Apply check passes (N === N). Account → N+1. 2. Tx B (replay) applies next. Edit populates `expectedPrior = N+1` (live state already advanced). Apply check passes (N+1 === N+1). Account → N+2. Both succeed. This defeats the entire cross-RPC safety net. Fix: Move populate into `assignNonce` itself (validation time), using the already-loaded account row + pending-mempool-count: `expectedPrior = account.nonce + pendingCount` This locks the value to the pre-block snapshot the sender's nonce was supposed to be at, NOT what it becomes at apply time. For the same scenario: 1. Tx A's edit carries `expectedPrior = 0`. Apply: account=0, expectedPrior=0 → match. Account → 1. 2. Tx B's edit also carries `expectedPrior = 0` (validated on a node that didn't yet see Tx A). Apply: account=1, expectedPrior=0 → MISMATCH → reject. Whole tx rolls back. For back-to-back same-sender submissions (k-th tx): `expectedPrior = account.nonce + (k-1)` matching the apply order because PR 2's mempool lookahead ensures each pending tx applies before this one. Hash compare safety: `endpointValidation` snapshots `tx.content.gcr_edits` at line 48 BEFORE `confirmTransaction` runs (which invokes `assignNonce`). My mutation hits the real array but the snapshot is the pre-mutation shape. Both sides of the compare are still stripped of `expectedPrior`, so signature integrity is preserved. The mutated array flows downstream into the apply pipeline carrying the validation-time-locked `expectedPrior`. Files: - src/libs/blockchain/routines/validateTransaction.ts: walk `tx.content.gcr_edits`, set `expectedPrior` on the matching `nonce` edit (sender address compared case-insensitive). - src/libs/blockchain/gcr/handleGCR.ts: revert the apply-time populate code added in the previous commit. Also drop the now- unused `isForkActive` and `getSharedState` imports. - docs/specs/audit-sweep-batch-c-nonce.md: rewrite the fork activation Post-fork bullet to describe the validation-time populate, plus explicit rationale for why apply-time is wrong. Update PR 3 file list to reflect the moved populate site. Verification: - tsc --noEmit clean. - Manual trace of the cross-RPC scenario now correctly rejects the second replay. * fix(audit-sweep): address Greptile + CodeRabbit review on PR #886 (greploop iter 1) Three real findings + three doc/comment fixes. Critical (CodeRabbit) — fork-gate the consensus rule GCRNonceRoutines.apply enforced the `expectedPrior` mismatch check unconditionally whenever the field was present. The spec says pre-fork nodes must ignore it, and the field is stripped from the hash compare in endpointValidation — so a client could attach `expectedPrior` on a pre-fork chain without breaking signature validation, and upgraded nodes would start rejecting edits that older nodes still accepted. Validator-split risk before the fork activates. Fix: wrap the mismatch check in `isForkActive(nonceEnforcement, blockHeight)` using `getSharedState.lastBlockNumber` (same gate source `assignNonce` uses). Pre-fork ignores the field entirely; post-fork enforces. Imports added: `isForkActive`, `getSharedState`. Major (CodeRabbit) / P1 (Greptile) — forge-account comparison bug In `assignNonce`'s populate loop, `edit.account` can be a forge-key object (non-string), but the comparison was `editAccount === senderAddress` where `senderAddress` is a lowercase hex string. Strict equality between an object and a string is always false, so the populate would silently skip for forge-format accounts and the cross-RPC safety net would never engage for those transactions. Fix: mirror the normalization already used in `GCRNonceRoutines`: `typeof edit.account === "string" ? edit.account : forgeToHex(edit.account)`, then `.toLowerCase()`. Now matches the sender regardless of how the SDK ships the account field. Doc / comment cleanup Three locations still carried "node fills it in at apply time" wording, contradicting the validation-time populate site. Updated in: - `GCRNonceRoutines.ts`: comment now describes the fork gate + validation-time source. - `endpointValidation.ts`: comment now points at `assignNonce` as the populate site. - `docs/specs/audit-sweep-batch-c-nonce.md`: Path A header rewritten as "node populates at validation time"; rationale paragraph now explains the apply-time draft was rejected, with the cross-RPC reasoning inline. Doc — PR 3 row scope The scope-table row for PR 3 listed "e2e test" as a deliverable, but the diff ships no test. Removed the "e2e test" item and added an explicit note that cross-RPC e2e is tracked as a follow-up PR. Also reworded "apply-time rejection" to "consensus- side rejection" to match what `GCRNonceRoutines` actually does (rejection at apply, populate at validation). Out of scope for this iter TOCTOU advisory-lock wrapper documented in `assignNonce`'s PR 2 docstring is still not landed; flagged in this review round but deferred to a follow-up PR since wrapping the validate + `Mempool.addTransaction` sequence in `pg_advisory_xact_lock` is its own structural change that affects every native-tx ingress path. Filed as PR 4 in the follow-up tracker. Verification: - `tsc --noEmit` clean. - Pre-fork trace: `expectedPrior` present on edit, fork inactive, `isForkActive` returns false → mismatch check skipped → legacy apply behaviour preserved bit-identically for re-sync. - Post-fork trace: forge-format `edit.account` now hex-coerces and matches the lowercased sender → `expectedPrior` populates correctly → `GCRNonceRoutines` enforces the mismatch reject. --------- Co-authored-by: tcsenpai <tcsenpai@discus.sh>
…ork testing pass Single document covering everything surfaced during the testing pass that ran 2026-05-26 → 2026-05-31 against dev.node2. Organised by topic, not chronology, so the team has one place to look up what was found, what was fixed, and what is still pending — without scrolling Telegram or hopping between PRs. Sections: - L2PS encryption nonce reuse (SDK #87 + 3 follow-up security findings) - Governance hash mismatch on dev.node2 (still broken; real fix in DEM-727) - Test-coverage gap that allowed the governance break to slip through - Side finding — nonces were not checked or incremented per tx (Hovhannes's batch C #884–#887, batch D #888 in flight) - UX surfacing for L2PS (Demo #11) - Operational risks still open - Linear ticket map across DEM-722 epic - Source material cross-links Cross-links the raw battery output and serializer analysis already in flight via Node #876. Closes DEM-729. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
PR 1 of the 3-PR batch C nonce-replay-protection sequence specified in
docs/specs/audit-sweep-batch-c-nonce.md.This commit ships the validation infrastructure behind the (still commented-out) caller in
confirmTransaction. No live behaviour change in this PR.4 files, +154 / -6.
Background
The 2026-05-28 bug-hunt audit flagged
assignNonceinvalidateTransaction.ts:362as a hardcoded stub returningtrue(// TODO Override for testing). Investigation during PR scoping found the nonce gap is 3-layered:src/libs/. SDK sendstx.content.nonce = currentNonce + 1, node persists it, never validates.GCRNonceRoutines.tscorrectly appliesnonce-typedGCREdits, but no code emits one. Account nonce stays at 0 forever.handleGCR.ts:910blindly applies nonce edits. Two block-included txs from the same sender both carryingtx.content.nonce = N+1both succeed (double-spend window for cross-RPC replays).Mempool hash-dedup blocks identical re-broadcasts only — signature is part of the tx hash. Does not block cross-RPC replay before block inclusion.
Full design + PR breakdown is in the spec doc landed by the prior commit on this branch.
What this PR ships
New fork:
nonceEnforcementsrc/forks/forkConfig.ts— newForkName+NonceEnforcementConfig(alias ofBaseForkConfig; no extra payload).activationHeight: nullso existing chains stay bit-identical until governance flips it.writeForkConfig+validateForkEntryswitch branches updated. Theneverexhaustiveness guard catches futureForkNameadditions at compile time.isForkActiveis generic, picks up the new name automatically.Real
assignNonceimplementation (validateTransaction.ts)true. Bit-identical to the previous hardcoded stub. Re-syncing pre-fork blocks is unaffected.tx.content.nonce === account.nonce + 1on the sender's GCR account.+1nonceGCREditbyHandleNativeOperations.handle()in PR 3, applied at consensus time byGCRNonceRoutines.confirmTransaction(lines 77-86) remains commented out in PR 1. PR 3 uncomments it once the consensus-side rejection (GCREdit.expectedPrior) is in place, so validation and the apply-time check ship together behind the same fork gate.Devnet
testing/devnet/genesis.devnet.jsongains anonceEnforcemententry withactivationHeight: 0so fresh devnet docker boots already post-fork. Matches the existingosDenominationpattern;wipe_and_reboot.sh/ docker--cleanflow needs no changes.Out of scope (subsequent PRs in this sequence)
assignNonceso back-to-back submissions from one sender are accepted in order.GCREdit.expectedPriorfield, nonce-edit emission inHandleNativeOperations, consensus-side rejection inGCRNonceRoutines, caller uncomment. Requires a single SDK type publish before merge (adds optionalexpectedPrior?: numberto thenonce-typedGCREditvariant; runtime SDK behaviour unchanged).Verification
tsc --noEmitproduces zero new errors in the four changed files.confirmTransactionbecause the caller remains commented out — the function is reachable only via direct import (currently none insrc/libs/).return truethe previous stub had.Test plan
bun installand confirm clean tscwipe_and_reboot.sh(or docker--clean); confirm[FORKS] Loaded fork nonceEnforcement activationHeight=0appears in logsassignNonce(tx)returnstrueregardless oftx.content.noncevalue (legacy preservation)Summary by CodeRabbit
Documentation
New Features
Tests