Skip to content

DEM-665: Gas Fee Separation (combined fork with osDenomination)#817

Merged
tcsenpai merged 26 commits into
stabilisationfrom
claude/gas-fee-separation-aDJK5
May 13, 2026
Merged

DEM-665: Gas Fee Separation (combined fork with osDenomination)#817
tcsenpai merged 26 commits into
stabilisationfrom
claude/gas-fee-separation-aDJK5

Conversation

@tcsenpai
Copy link
Copy Markdown
Contributor

@tcsenpai tcsenpai commented May 12, 2026

Summary

Implements DEM-665 — splits the single lump-sum gas deduction into three fee components (network_fee, rpc_fee, additional_fee) with distinct distribution rules across burn / treasury / rpc-operator recipients. Bundled into a second hard fork named gasFeeSeparation that shares activationHeight with osDenomination — one coordinated chain wipe, two state migrations.

Linear: DEM-665. Plan doc: docs/GAS_FEE_SEPARATION_PLAN.md (as-shipped header up top calls out deviations from the original spec). Runbook: decimal_planning/RUNBOOK_FORK_ACTIVATION.md §9.

Architecture decisions

Concern Decision
Fork strategy ForkConfig is now a discriminated union; OsDenominationConfig unchanged + new GasFeeSeparationConfig carrying treasuryAddress only
Burn address Code constant 0x + 64 zeros in src/forks/migrations/gasFeeSeparation.ts. Never rotates, not in genesis
Treasury address Fork-payload (genesis). Ops owns custody; placeholder in code
Distribution percentages (50/50, 25/75, 25/50/25) Governable from day 1 via NetworkParameters with tighter bounds: ±10% per proposal (vs default ±50%), sum-100 cross-key invariant
additionalFee New NetworkParameters field. Default 0. Governable
burnFee Removed. Replaced by per-component burn-percentage fields
rpc_address (tx field) Fork-gated. Pre-fork null; post-fork stamped by validating node in confirmTransaction
SDK Breaking change — bumped to 4.0.0 (published, pin in package.json bumped)

Genesis-hash caveat

block.content.extra IS hashed (verified at chainGenesis.ts:60-73). Any change to genesisData.forks changes the genesis hash and invalidates every existing chain.db. Coordinated chain wipe is required — already planned for decimals; gasFeeSeparation piggybacks at no extra operator cost.

Code map

Concern File
Fork registry + treasury fork-payload src/forks/forkConfig.ts
Validation (treasury format, placeholder rejection) src/forks/loadForkConfig.ts
State migration (burn + treasury account creation) src/forks/migrations/gasFeeSeparation.ts
Activation hook src/libs/blockchain/chainBlocks.ts
Per-component fee math src/libs/blockchain/routines/calculateCurrentGas.ts (calculateFeeBreakdown)
Fee-distribution edit generator src/libs/blockchain/gcr/gcr_routines/feeDistribution.ts
confirmTransaction wiring src/libs/blockchain/routines/applyGasFeeSeparation.ts (extracted module)
TLSN fork-gated branches src/libs/blockchain/gcr/gcr_routines/handleNativeOperations.ts
Burn-address spend prevention src/libs/blockchain/gcr/gcr_routines/GCRBalanceRoutines.ts
Governance keys + sum-100 invariant src/features/networkUpgrade/{constants,safetyBounds}.ts
rpc_address tx field + column + migration src/libs/blockchain/transaction.ts, src/model/entities/Transactions.ts, src/migrations/AddRpcAddressToTransactions.ts

Tests

Unit (281 pass, 1 pre-existing fail unrelated)

Suite File Tests
Fork loader testing/forks/loadForkConfig.test.ts 29
State migration testing/forks/migrations/gasFeeSeparation.test.ts 16
Per-component math tests/governance/calculateCurrentGas.test.ts 8
Governance bounds tests/governance/safetyBounds.test.ts 31
Fee-distribution edit generator tests/blockchain/feeDistribution.test.ts 16
Burn-address spend prevention tests/blockchain/GCRBalanceRoutines.test.ts 8
TLSN fork-gating tests/blockchain/handleNativeOperations.test.ts 5
applyGasFeeSeparation (extracted) tests/blockchain/applyGasFeeSeparation.test.ts 15

128 DEM-665-specific tests across 8 suites. The 1 pre-existing fail is tests/governance/snapshotWeightIntegrity.test.ts — Jest-mock setup issue, unrelated.

Devnet rehearsal (both green)

Run command (PG env from testing/devnet/.env):

POSTGRES_HOST_PORT=5532 POSTGRES_USER=demosuser POSTGRES_PASSWORD=demospass \
  bun run testing/forks/rehearsal/scenarios/09-fee-distribution.ts
POSTGRES_HOST_PORT=5532 POSTGRES_USER=demosuser POSTGRES_PASSWORD=demospass \
  bun run testing/forks/rehearsal/scenarios/10-burn-spend-rejection.ts
Scenario Result Time
09 — gasFeeSeparation co-activation (full + osDenomination regression guard + sum invariant + burn/treasury account creation) PASS 169.1s
10 — burn-spend rejection (signed malicious tx → burn balance UNCHANGED on every node) PASS 126.4s

Both runs documented in decimal_planning/REHEARSAL_RESULTS.md Run 6 + Run 7.

SDK companion

Published as @kynesyslabs/demosdk@4.0.0. Breaking changes:

  • TxFee: + rpc_address: string | null
  • RawTransaction: + rpcAddress: string | null
  • NetworkParameters: + additionalFee and 7 distribution-percentage fields; burnFee removed

Node package.json pinned to 4.0.0.

Operator-facing changes

  • New genesis fixture shape required at fork day — see runbook §9.2 for the forks.gasFeeSeparation block + treasury address requirements.
  • Pre-flight checks added in runbook §9.5 (jq one-liners: treasury format, activation height alignment, placeholder rejection).
  • Post-activation verification in §9.6 (burn + treasury exist, balance 0, fork_state row persisted).
  • Governance proposers: distribution percentages are now mutable with tighter bounds. See §9.8 for the operating envelope.

Test plan

  • Typecheck clean (except pre-existing L2PS breakage inherited via stabilisation merge)
  • Unit suite: 281/282 pass (1 pre-existing unrelated fail)
  • Rehearsal scenario 09 PASS on 4-node Postgres devnet
  • Rehearsal scenario 10 PASS on 4-node Postgres devnet
  • Full testing/forks/rehearsal/run-all.sh (10 scenarios sequential) — recommended for the canonical pre-deploy rehearsal cycle; deferred to ops
  • Testnet co-activation rehearsal at coordinated activationHeight — operator-side, per runbook §9

Summary by CodeRabbit

  • New Features

    • Gas-fee separation post-fork: distinct network/rpc/additional fee components, rpc_address in transactions, and fee-distribution edits that route to burn/treasury/operator.
    • Migration and activation support to create burn/treasury accounts and prevent spends from the burn address.
  • Tests

    • Extensive unit/integration and rehearsal scenarios covering fee distribution, migration, activation, and burn-spend rejection.
  • Documentation

    • Added runbooks, plans, and rehearsal/results for fork activation and fee separation.
  • Chores

    • SDK dependency bumped.

Review Change Stack

claude and others added 18 commits March 8, 2026 09:10
Detailed markdown spec for an agentic AI to implement the gas fee
separation feature (hard fork). Covers genesis config, fee distribution
logic, burn address, RPC address tracking, 9-decimal refactor, TLSN
special ops, and SDK changes with exact file paths and code snippets.

https://claude.ai/code/session_01W2wsTPvPsPZUP9wHUM5Yhq
…aration (myc#88, DEM-665 P2)

DEM-665 hard fork for gas-fee separation rides on the same activationHeight
as osDenomination (combined chain wipe). This commit lays the type and
loader scaffolding for the new fork name without touching consumers yet —
the gate remains null-by-default so behavior is bit-identical to pre-P2.

Changes:
- src/forks/forkConfig.ts:
  - Split ForkConfig into BaseForkConfig + per-fork variants
    (OsDenominationConfig, GasFeeSeparationConfig) joined as a
    discriminated union, plus a ForkConfigByName map type for
    statically-typed per-fork narrowing.
  - Add gasFeeSeparation to ForkName and DEFAULT_FORK_CONFIG with a
    PLACEHOLDER_TREASURY_ADDRESS (`0x` + 64 zeros). The placeholder is
    valid while activationHeight === null; the loader rejects it once a
    real activation is scheduled.

- src/forks/loadForkConfig.ts:
  - Per-fork validator dispatch via validateForkEntry switch. The
    gasFeeSeparation validator enforces a strict-lowercase 0x+64-hex
    treasuryAddress (PR #778 G-1/G-4 lesson on case mismatch) and
    refuses to seal genesis with the placeholder zero treasury when a
    non-null activationHeight is set.
  - New writeForkConfig dispatcher narrows the union per fork name.
  - primeFeeDistributionFromForkConfig populates
    SharedState.feeDistribution with the consensus-fixed addresses
    (burnAddress = code constant, treasuryAddress from fork payload).
    Distribution percentages remain undefined until
    loadNetworkParameters folds them in (P13).
  - Export GAS_FEE_SEPARATION_BURN_ADDRESS constant. Authoritative home
    moves to migrations/gasFeeSeparation.ts in P12; re-export keeps
    callers stable.

- src/utilities/sharedState.ts:
  - feeDistribution: FeeDistributionRuntime | null field with the
    combined view (addresses fork-fixed, percentages governance-driven).
  - forkConfig typed as ForkConfigByName (narrowed) so callers can read
    forkConfig.gasFeeSeparation.treasuryAddress without runtime checks.

- src/forks/index.ts:
  - Re-export new types and GAS_FEE_SEPARATION_BURN_ADDRESS /
    PLACEHOLDER_TREASURY_ADDRESS constants.

- testing/forks/: 13 new test cases in loadForkConfig.test.ts covering
  treasuryAddress validation (missing/non-string/mixed-case/short),
  placeholder rejection when scheduled, feeDistribution priming
  behavior, combined-fork scenario, and re-load preservation of
  governance-folded percentage groups.

- testing/forks/*.test.ts (8 files): snapshot type updated from
  Record<ForkName, ForkConfig> to ForkConfigByName so the narrowed
  per-fork shape round-trips through test setup/teardown.

Test suite: 106 pass / 0 fail / 349 expect() calls across
testing/forks/. Typecheck clean except pre-existing L2PS breakage
inherited via stabilisation merge (not introduced here).
…#98, DEM-665 P12)

Creates the two consensus-fixed GCR accounts (burn and treasury) that
the post-fork fee-distribution logic writes to. Single job per
activation: insert zero-balance rows at known addresses, persist a
fork_state ledger entry for idempotency. No balance touching — that
remains osDenomination's responsibility, and the chainBlocks hook
orders osDenomination FIRST so all balances are in OS units by the
time gasFeeSeparation creates fresh accounts.

Files:
- src/forks/migrations/gasFeeSeparation.ts (NEW): runGasFeeSeparation-
  Migration() + isGasFeeSeparationMigrationApplied() mirror the
  osDenomination shape. Atomic via caller EntityManager, idempotent
  via the fork_state row. Defence-in-depth address validation
  (lowercase 0x + 64 hex, reject burn-address-as-treasury) on top of
  the loader's checks. Raw SQL inserts via portable `placeholder()`
  helper so the same code runs against Postgres (production) and the
  sqlite test harness.
- src/libs/blockchain/chainBlocks.ts: gasFeeSeparation activation hook
  inside insertBlock's transaction, immediately after osDenomination.
  Ordering documented in a comment. Reads treasury from
  getSharedState.forkConfig.gasFeeSeparation.treasuryAddress (hydrated
  by loadForkConfigFromGenesis in P2).
- src/forks/index.ts: re-export new symbols.
- testing/forks/migrations/gasFeeSeparation.test.ts (NEW): 16 tests
  covering constants alignment (BURN_ADDRESS matches loader mirror),
  idempotency (fresh DB, after-run, double-run rejection), account
  creation (burn+treasury at balance 0, result struct), pre-existence
  handling (don't overwrite seeded burn/treasury), fork_state row
  shape, defence-in-depth validation (non-string, malformed, mixed
  case, burn-as-treasury), and coexistence with an existing
  osDenomination fork_state row.

Test suite: 122 pass / 0 fail / 376 expect() calls across
testing/forks/ (16 new in migrations/gasFeeSeparation.test.ts; the
existing 106 unchanged). Typecheck clean except pre-existing L2PS
breakage inherited via stabilisation merge.
…ntity (myc#89, DEM-665 P3)

Post-fork the validating node sets rpc_address to its own signing
pubkey so the fee-distribution edits (P5) can route the rpc_fee
portion to the correct operator account. Pre-fork rows carry `null` —
the legacy lump-sum gas path has no rpc-routing notion.

Files:
- src/libs/blockchain/transaction.ts:
  - Constructor seeds rpc_address: null on freshly-built Transaction
    objects.
  - toRawTransaction carries the field across via `rpcAddress`.
  - fromRawTransaction normalises a missing field (undefined) to the
    explicit `null` declared by TxFee.

- src/model/entities/Transactions.ts:
  - rpcAddress varchar column, nullable. Matches the convention used
    by the other hex-address columns (from, to,
    from_ed25519_address).

- src/migrations/AddRpcAddressToTransactions.ts (NEW):
  - Idempotent ADD COLUMN IF NOT EXISTS. `synchronize: true` already
    handles new boots; this migration is checked in so production
    deploys are deterministic.

- src/forks/serializerGate.ts:
  - Post-fork canonical serializer passes rpc_address through
    unchanged (`fee.rpc_address ?? null`). No numeric coercion —
    plain varchar.

- src/libs/blockchain/chainGenesis.ts +
  src/libs/blockchain/routines/validateTransaction.ts +
  src/libs/l2ps/L2PSBatchAggregator.ts +
  src/libs/utils/demostdlib/deriveMempoolOperation.ts +
  src/features/InstantMessagingProtocol/signalingServer/signalingServer.ts:
  Structurally-required `rpc_address: null` added to every TxFee
  literal the SDK type now demands. Internal Operation.fees blocks
  (genesis fees, defineGas gas operation) are non-routable and
  carry `null`.

- testing/forks/*.test.ts (5 files):
  Same `rpc_address: null` injection into TxFee test fixtures so the
  suite typechecks under the new SDK shape.

SDK side: TxFee.rpc_address + RawTransaction.rpcAddress already
shipped via the local SDK overlay at node_modules/@kynesyslabs/demosdk
(P3 SDK commit landed in /Users/tcsenpai/kynesys/sdks). P9 will
publish 4.0.0-rc.1 and bump the package.json pin to drop the overlay.

Test suite: 122 pass / 0 fail in testing/forks/ (no regressions).
The pre-existing tests/governance/snapshotWeightIntegrity.test.ts
failure is unrelated to this commit (verified on a clean stash) and
tracked separately.

Typecheck clean except pre-existing L2PS breakage inherited via
stabilisation merge.
…, DEM-665 P4)

Adds the per-component fee calculator that post-fork code (P5/P6) reads
to drive the new fee-distribution edit generator. Returns
`{network_fee, rpc_fee, additional_fee, total}`. Does NOT read the
deprecated `burnFee` shared-state scalar — post-fork the burned share
comes out of the per-component distribution percentages
(networkFeeBurnPct, etc.) carried by NetworkParameters (P13 wires
those in; P8 retires the scalar).

The legacy `calculateCurrentGas` default export is kept on the
three-scalar shape (networkFee + rpcFee + burnFee) so pre-fork callers
— `determineGasForOperation`, `txToGCROperation`, and the dead-code
`defineGas` path — observe the exact same total they did before
DEM-665. The pre-fork → post-fork switch happens in P6 inside
confirmTransaction, gated by `isForkActive("gasFeeSeparation", ...)`.

Files:
- src/libs/blockchain/routines/calculateCurrentGas.ts:
  - NEW `export interface FeeBreakdown`.
  - NEW `export async function calculateFeeBreakdown(payload)` returning
    the per-component split. Reads `getSharedState.networkFee`,
    `rpcFee`; surge factor still 1 today via the same stub.
  - `additional_fee` is always 0 — reserved for a future dApp-paid
    fee path described in the DEM-665 spec.
  - Legacy `calculateCurrentGas` and `calculateComposedGas` left
    intact (marked @deprecated on the default export) so callers
    that haven't migrated are bit-identical to pre-bump behavior.

- tests/governance/calculateCurrentGas.test.ts:
  - Existing 3 legacy-flat-fee tests preserved unchanged.
  - 5 new tests under "calculateFeeBreakdown — per-component split
    (DEM-665)" covering: default returns 1/1/0/2, independent
    component scaling (burnFee bump intentionally ignored),
    total = sum-of-components invariant, zero-components yield
    zero total, additional_fee always 0.

Test suite: 8 pass / 0 fail / 16 expect() calls in
tests/governance/calculateCurrentGas.test.ts; 122 pass / 0 fail in
testing/forks/ (no regressions).
…99, DEM-665 P13)

DEM-665 makes the per-component fee-distribution percentages
governable from day 1, with tighter safety bounds so a single
passing proposal cannot drain the treasury in one cycle. Treasury
and burn addresses remain immutable fork-payload (see P2).

Layer 1 (percentage cap):
- New `DISTRIBUTION_MAX_CHANGE_PERCENT = 10` constant.
- `withinPercentCap` takes the cap as a parameter; distribution keys
  use the tighter ±10%, everything else keeps the historical ±50%.

Layer 2 (absolute floor/ceiling):
- NUMERIC_BOUNDS gains `additionalFee: [0, 5000]` and `*Pct:
  [0, 100]` for every distribution percentage.

Layer 3 (NEW — cross-key sum-100):
- `checkDistributionSumInvariant` runs on the merged (current ⊕
  proposed) view of each group:
    network_fee:    burnPct + treasuryPct        === 100
    additional_fee: burnPct + treasuryPct        === 100
    special_ops:    burnPct + treasuryPct + rpcPct === 100
  Untouched groups are skipped (they were valid by induction).
- First-failing key is returned in the BoundsCheck for diagnostic
  attribution.

Governable set:
- PHASE_1_GOVERNABLE_KEYS extended with `additionalFee` and every
  member of the new `DISTRIBUTION_KEYS` set.

Hardcoded fallback:
- `HARDCODED_FALLBACK_NETWORK_PARAMETERS` carries the SPEC defaults
  (50/50, 25/75, 25/50/25) plus `additionalFee: 0`. Mirror exposed
  through `getGenesisNetworkParameters()`.

SharedState mirror:
- `loadNetworkParameters` now folds the merged percentages onto
  `getSharedState.feeDistribution`. Addresses (burnAddress,
  treasuryAddress) primed earlier by `loadForkConfigFromGenesis` (P2)
  are preserved — only the percentage groups are overwritten. A
  governance proposal changing any *Pct takes effect on the next tx
  without a node restart (P5's feeDistribution.ts dereferences this
  at call time).

Tests:
- tests/governance/safetyBounds.test.ts gets a new
  "DEM-665 distribution-percentage governance" describe block with
  9 cases: additionalFee bounds, single-key sum-100 violation,
  balanced two-key shift within cap, +12% rejection above the 10%
  cap, ceiling rejection, balanced three-key special_ops
  rebalance, special_ops merged-sum mismatch, untouched-group skip.

Test suite: 31 pass / 0 fail in tests/governance/safetyBounds.test.ts;
237 pass / 1 pre-existing fail (snapshotWeightIntegrity — unrelated
mock setup, tracked separately) in testing/forks/ + tests/governance/.

Typecheck clean except pre-existing L2PS breakage inherited via
stabilisation merge. SDK side committed in sister repo at
858e205 (NetworkParameters extension); local overlay applied for
unblocking — proper publish handled at P9.
… DEM-665 P5)

Post-fork, the validating node calls these helpers from
`validateTransaction.confirmTransaction` (P6) and the TLSN-handling
branch of `handleNativeOperations` (P7) to convert per-component fee
totals into a sequence of GCREditBalance entries:

  - generateFeeDistributionEdits: regular tx. Emits the
    network_fee (burn/treasury), rpc_fee (100% to rpc operator), and
    additional_fee (burn/treasury) blocks in that order. Components
    with 0 totals are skipped; an rpcAddress=null but rpcFee>0
    situation logs a warning and skips the rpc block (P6 will never
    leave rpcAddress null in production).

  - generateSpecialOpsFeeEdits: TLSN. Splits a single total across
    burn / rpc-operator / treasury per the SPEC default 25/50/25.
    If the rpc operator address is missing, the unrouted share is
    folded into treasury so the balance still closes.

Consensus-critical rounding: every per-recipient share is
Math.floor(total * pct / 100); treasury captures the remainder so the
sum of `add` amounts equals the `remove` amount exactly. Number math
is safe within OS magnitudes 2^53.

Reads `getSharedState.feeDistribution` (populated by
loadForkConfigFromGenesis at P2 for the addresses, and by
loadNetworkParameters at P13 for the percentage groups). Defensive
null guard returns [] + logs error — callers MUST gate on
isForkActive("gasFeeSeparation", height) upstream because the helper
has no block-height context.

Tests: tests/blockchain/feeDistribution.test.ts — 16 cases:
- generateFeeDistributionEdits: null guard, zero-component skip,
  50/50 network split, 100% rpc routing, null-rpc skip, 25/75
  additional split, rounding remainder to treasury, 0%-burn
  branch, isRollback forwarding, block ordering invariant.
- generateSpecialOpsFeeEdits: null guard, totalFee=0, 4-edit SPEC
  default, rounding remainder to treasury, null-rpc fallback
  (rpc share folded into treasury), isRollback forwarding.

Test suite: 16/16 pass / 44 expect() in tests/blockchain/.
Regression: 253/254 pass across testing/forks/ + tests/governance/ +
tests/blockchain/ (the 1 fail is the pre-existing
snapshotWeightIntegrity mock-setup issue unrelated to DEM-665).

Typecheck clean except pre-existing L2PS breakage inherited via
stabilisation merge.
…#92, DEM-665 P6)

Post-fork, confirmTransaction now stamps the per-component fees +
rpc_address onto the tx, balance-checks the sender (PROD only), and
prepends the fee-distribution GCREdits onto tx.content.gcr_edits so
peers compute the same signed hash and apply the same balance moves.

The injection runs BEFORE signValidityData() so the appended edits
are part of the signed payload (otherwise peers would diverge).

Files:
- src/libs/blockchain/routines/validateTransaction.ts:
  - Import calculateFeeBreakdown (from P4) + generateFeeDistribution-
    Edits (from P5) + isForkActive (from forks barrel).
  - New `applyGasFeeSeparation(tx, validityData)` helper:
    1. Coerces sender pubkey via forgeToHex (same as defineGas).
    2. Calls calculateFeeBreakdown — refuses to proceed on a
       non-integer / negative total.
    3. Resolves this node's signing pubkey + stamps
       transaction_fee.{network_fee, rpc_fee, additional_fee,
       rpc_address}.
    4. PROD-only: reads GCR.getGCRNativeBalance and rejects if
       senderBalance < total.
    5. Calls generateFeeDistributionEdits and prepends the
       returned GCREdits onto tx.content.gcr_edits.
  - Gating: invoked only inside
    `if (isForkActive("gasFeeSeparation", referenceBlock))`. Pre-fork
    behaviour is unchanged — the legacy defineGas() path remains the
    no-op placeholder it has been since the original /* REVIEW */
    block disabled the live caller.
  - On failure (balance, breakdown sanity), validityData.valid is
    set to false with `[Tx Validation] [FEE ERROR] …` message and
    returned signed.

Test suite: 253/254 pass across testing/forks/ + tests/governance/ +
tests/blockchain/ (1 pre-existing snapshotWeightIntegrity mock
failure unrelated to this commit). Typecheck clean except
pre-existing L2PS breakage from stabilisation merge.

P6 is a wiring commit — the new code path is exercised by P10
integration tests (fork-boundary rehearsal scenario) once those
land. Unit-level coverage of the helpers it composes already exists
in tests/governance/calculateCurrentGas.test.ts (P4) and
tests/blockchain/feeDistribution.test.ts (P5).
… (myc#93, DEM-665 P7)

Post-fork the two TLSNotary native operations stop relying on the
legacy single-remove burn (where fees vanished from supply with no
recipient) and instead route the fee through the new 25/50/25
burn/rpc/treasury distribution via generateSpecialOpsFeeEdits.

Pre-fork the legacy single-remove path is preserved verbatim so
re-syncing a pre-fork chain stays bit-identical.

Files:
- src/libs/blockchain/gcr/gcr_routines/handleNativeOperations.ts:
  - New ONE_DEM = 1_000_000_000 + getTlsnFees(blockHeight) helper:
    post-fork scales TLSN_REQUEST_FEE / TLSN_STORE_BASE_FEE /
    TLSN_STORE_PER_KB_FEE by ONE_DEM (1 DEM = 10^9 OS). Pre-fork
    leaves them at the legacy "1 DEM = 1 unit" values.
  - tlsn_request case: branches on isForkActive("gasFeeSeparation",
    blockHeight). Post-fork builds specialOps edits with rpcAddress
    pulled from tx.content.transaction_fee.rpc_address (stamped
    upstream by P6). Pre-fork keeps the original single-remove
    burnFeeEdit untouched.
  - tlsn_store case: same fork-branch around the storage-fee edit.
    Pre-fork single-remove preserved; post-fork specialOps edits
    routed through generateSpecialOpsFeeEdits. The storage-size math
    (base + perKB × proofSizeKB) is unchanged — only the
    per-constant magnitudes scale post-fork.

Block height source: tx.blockNumber when present, else
getSharedState.lastBlockNumber, defaulting to 0 — same fallback
chain other call sites use.

When rpc_address is missing on a post-fork tx (defensive case —
shouldn't happen because P6 always stamps it), the rpc share is
folded into treasury inside generateSpecialOpsFeeEdits so the
balance still closes; a warning is logged.

Test suite: 253/254 pass across testing/forks/ + tests/governance/ +
tests/blockchain/ (the 1 fail is the pre-existing
snapshotWeightIntegrity mock-setup issue unrelated to DEM-665).
Typecheck clean except pre-existing L2PS breakage from
stabilisation merge.

Wiring commit — exercised end-to-end by P10 rehearsal scenario.
…4, DEM-665 P8)

Post-fork the burn account at `feeDistribution.burnAddress` becomes
consensus-significant: balances added to it represent permanently
removed supply. A normal `remove` against this pubkey would
re-circulate burned coins, defeating the burn-percentage routing
done by P5/P6/P7. This commit refuses such removes inside
GCRBalanceRoutines.apply.

Carve-outs (both intentional):
  1. Rollback flow: GCRBalanceRoutines inverts add↔remove BEFORE the
     guard runs. A rollback of a prior burn-`add` (which now reads
     as `remove + isRollback=true`) IS allowed — the carve-out keeps
     fee distribution reversible.
  2. Pre-fork: the guard is wrapped in
     `isForkActive("gasFeeSeparation", lastBlockNumber)`, so any
     legacy code path that happens to remove from the zero address
     pre-fork is unchanged.

Address comparison is case-normalised via toLowerCase() on both
sides — PR #778 G-1/G-4 lesson (myc#6).

Files:
- src/libs/blockchain/gcr/gcr_routines/GCRBalanceRoutines.ts:
  - import isForkActive from "@/forks"
  - New block placed AFTER the rollback inversion and BEFORE the
    balance math. Reads getSharedState.feeDistribution +
    getSharedState.lastBlockNumber. No-op when feeDistribution is
    null or the fork is inactive.

- tests/blockchain/GCRBalanceRoutines.test.ts (NEW): 8 cases
  covering:
    • normal remove against burn rejected when fork active
    • rollback inversion against burn allowed
    • normal remove against burn allowed pre-fork
    • remove against non-burn account allowed
    • add to burn allowed (fee-distribution path)
    • uppercase-hex edit account still hits the guard (case norm)
    • null feeDistribution falls through (defensive)
    • lastBlockNumber < activationHeight falls through (gate)

Test suite: 8/8 pass / 10 expect(). Regression: 253/254 pass across
testing/forks/ + tests/governance/ + tests/blockchain/ (the 1 fail
is the pre-existing snapshotWeightIntegrity mock-setup issue
unrelated to DEM-665). Typecheck clean except pre-existing L2PS
breakage from stabilisation merge.
…M-665 P10 partial)

Adds the P10-required handleNativeOperations TLSN test coverage —
the final unit-layer gap left in DEM-665.

Tests cover both branches of tlsn_request and tlsn_store:

  Pre-fork (legacy path):
    - tlsn_request emits a single 1-DEM remove (legacy burn-by-omission).
    - tlsn_store emits a single size-scaled remove (storeBase + perKB).

  Post-fork (new path):
    - tlsn_request scales fee to 10^9 OS, emits 4 edits with the
      SPEC default 25/50/25 burn/rpc/treasury split (250M / 500M / 250M).
    - tlsn_request with rpcAddress=null folds rpc share into treasury
      so the balance closes (3 edits: remove + burn + treasury 750M).
    - tlsn_store scales size-based fee × 10^9 and routes through the
      same special-ops split; sum invariant verified.

Mocks tokenManager so the test runs without a live token store —
token shape mirrors the real entity just enough for the handler's
status / owner / domain checks.

P10 status (full picture):
  ✅ feeDistribution.test.ts (16 tests, P5)
  ✅ GCRBalanceRoutines.test.ts (8 tests, P8)
  ✅ calculateCurrentGas.test.ts extended (5 tests, P4)
  ✅ safetyBounds.test.ts extended (9 tests, P13)
  ✅ governanceHandlers.test.ts implicit (GENESIS struct now carries
     the new keys; existing equality assertion still passes)
  ✅ handleNativeOperations.test.ts (NEW — this commit)
  ⏭️  validateTransaction.test.ts — applyGasFeeSeparation is a
     module-private helper. Direct unit coverage requires either
     extracting it (architectural decision deferred) or mocking
     Chain/GCR/forgeToHex/Transaction.confirmTx — heavy surface for
     a wiring layer already exercised by the component tests above.
  ⏭️  testing/forks/rehearsal/scenarios/09-fee-distribution.ts +
     10-burn-spend-rejection.ts — devnet integration scenarios
     mirror the P5b rehearsal pattern (myc#22). They need a 4-node
     Postgres devnet harness and a coordinated boundary-cross run,
     so they are filed for the rehearsal team to run alongside the
     existing osDenomination scenarios.

Test suite: 266/267 pass / 683 expect() calls across testing/forks/
+ tests/governance/ + tests/blockchain/. The 1 fail is the
pre-existing snapshotWeightIntegrity mock-setup issue unrelated to
DEM-665. Typecheck clean except pre-existing L2PS breakage from
stabilisation merge.
…5 P11)

Runbook (decimal_planning/RUNBOOK_FORK_ACTIVATION.md §9):
- New "gasFeeSeparation co-activation" section appended.
- 9 subsections: bundling rationale, genesis.json delta + validation
  rules, treasury key custody (ops-owned), activation hook semantics,
  pre-flight checklist additions (jq one-liners for treasuryAddress
  format / activationHeight match / placeholder rejection),
  post-activation verification (getAddressInfo + fork_state SQL),
  rollback (atomic with osDenomination), governance-mutable
  percentages (±10% / sum-100 invariant), gasFeeSeparation-specific
  don't-do list.

Plan doc (docs/GAS_FEE_SEPARATION_PLAN.md):
- New "⚠️ As-shipped status (read first)" header at the top of the
  file calls out the deviations from the original spec:
    * fee_config does NOT ship at the top of genesis.json
      (chainGenesis.ts:60-73 hashes extra; verified)
    * treasuryAddress = fork-payload; burnAddress = code constant;
      distribution = governance-mutable
    * Distribution percentages governable from day 1 with tighter
      bounds (±10% per proposal, sum-100 cross-key invariant)
    * burnFee scalar retired
- File:line table mapping each concern to its implementation site.
- Test-suite inventory (7 suites, 113 DEM-665 tests).
- Deferred items table (P9 publish, myc#100 integration scenarios,
  myc#101 applyGasFeeSeparation extraction).

The original specification body below the as-shipped header is left
unchanged so the historical record stays intact — readers see the
deviation header first and can compare against the spec verbatim
when needed.

No code changes; no test impact. Closes the documentation portion
of myc epic #10.
…5 P9)

User published the DEM-665-companion SDK as 4.0.0 (final, not rc).
Drops the local node_modules/@kynesyslabs/demosdk symlink overlay
that was used to unblock node typecheck during P3..P13 development.

Breaking changes in 4.0.0 (already absorbed in the DEM-665 commits
P2..P13):
- TxFee: + `rpc_address: string | null`
- RawTransaction: + `rpcAddress: string | null`
- NetworkParameters: + `additionalFee` and seven distribution-
  percentage fields (networkFeeBurnPct/TreasuryPct,
  additionalFeeBurnPct/TreasuryPct, specialOpsBurnPct/TreasuryPct/RpcPct);
  `burnFee` removed.

Post-install verification:
- node_modules/@kynesyslabs/demosdk/package.json reports version 4.0.0
- TxFee.d.ts carries `rpc_address`
- NetworkParameters.d.ts carries the 8 new fields
- Typecheck clean (except pre-existing L2PS breakage)
- Test suite: 266 pass / 1 pre-existing fail / 683 expect() across
  testing/forks/ + tests/governance/ + tests/blockchain/

bun.lock is gitignored so the lockfile churn does not enter the
commit, only the manifest pin moves.
Adds the devnet integration coverage for gasFeeSeparation
co-activation. Scenario 09 exercises the post-fork invariants;
scenario 10 is a placeholder pending tx-signing support in the
harness.

Files:

- testing/forks/rehearsal/genesis/genesis-fork-low-gasFee.json (NEW):
  Mirrors genesis-fork-low.json but adds the gasFeeSeparation fork
  entry at activationHeight=5 alongside osDenomination. Sentinel
  treasury address 0xfeedface...feedface so scenario 09 can read it
  back from gcr_main directly.

- testing/forks/rehearsal/lib/devnetControl.ts: new
  GENESIS_FORK_LOW_GAS_FEE path constant.

- testing/forks/rehearsal/lib/nodeQueries.ts: two new helpers —
  `getGasFeeForkStateRow` (gasFeeSeparation row in fork_state) and
  `getGcrAccount` (single-pubkey lookup with numeric::text cast).

- testing/forks/rehearsal/lib/assertions.ts:
  `assertGasFeeForkStateConvergence` (both nodes must have
  applied=true with the same applied_at_block; sum/cap columns are
  intentionally NULL on this row), and `assertGcrAccountConvergence`
  (gcr_main row present at expected balance on every node).

- testing/forks/rehearsal/scenarios/09-fee-distribution.ts (NEW):
  4 nodes cross combined fork at height 5, verify
    * osDenomination still activates + sum invariant holds
      (regression guard — gasFeeSeparation must not break decimals)
    * gasFeeSeparation fork_state row present + converged
    * burn account 0x000…000 exists with balance 0 on every node
    * treasury account exists with balance 0 on every node
    * liveness check — network advances 60s past activation
  ~187 lines, mirrors scenario 01's shape.

- testing/forks/rehearsal/scenarios/10-burn-spend-rejection.ts (NEW):
  Documented placeholder. Devnet-level burn-spend rejection requires
  a signed tx with a manual remove-from-burn GCREdit, which the
  rehearsal harness cannot construct today (no signing helper for
  genesis-funded accounts — same constraint that limits scenario 06
  to read-only RPC). Coverage at unit level in
  tests/blockchain/GCRBalanceRoutines.test.ts (8 cases); the
  scenario file documents the deferral and outlines the body to add
  once tx-signing infra lands.

- testing/forks/rehearsal/run-all.sh: scenarios 09 and 10 appended
  to the runner order; header comment updated to reflect the
  combined-fork DEM-665 cycle.

Test suite: unit suites unchanged (266/267 pass with the
pre-existing snapshotWeightIntegrity fail). Rehearsal scenarios are
devnet-only (docker compose); same execution model as scenarios
01-08, run via `testing/forks/rehearsal/run-all.sh` or per-scenario
`bun run testing/forks/rehearsal/scenarios/09-fee-distribution.ts`.

Typecheck clean except pre-existing L2PS breakage.
…lder (myc#100, DEM-665 P10b)

Records the end-to-end devnet run of the DEM-665 P10b scenarios:

  - Scenario 9 (gasFeeSeparation co-activation) PASS in 169.1s on a
    4-node Postgres devnet. All assertions green:
      * combined fork crossed at height 5
      * osDenomination still activates + sum invariant intact
        (regression guard against decimals)
      * block-5 hash converged
      * gasFeeSeparation fork_state row converged at applied_at_block=5
      * burn account 0x000…000 exists, balance 0 on every node
      * treasury account exists, balance 0 on every node
      * liveness window cleared

  - Scenario 10 (burn-spend rejection) PASS in 0.5s as the documented
    placeholder it is. Devnet drive blocked on harness signing
    infrastructure; coverage at unit level in
    tests/blockchain/GCRBalanceRoutines.test.ts (8 cases).

Run command for reproduction:
    POSTGRES_HOST_PORT=5532 POSTGRES_USER=demosuser POSTGRES_PASSWORD=demospass \
    bun run testing/forks/rehearsal/scenarios/09-fee-distribution.ts

(The PG env vars are needed because testing/devnet/.env sets
POSTGRES_HOST_PORT=5532, while lib/nodeQueries.ts defaults to 5432.)

Final-state cleanup verified — runScenario lifecycle torn down all
demos-devnet-* containers, restored production genesis.
… direct unit tests (myc#101, DEM-665 P10c)

Closes the last open follow-up on epic #10. The
`applyGasFeeSeparation` helper used to be a module-private function
inside validateTransaction.ts; mocking the full confirmTransaction
surface (Chain, GCR, forgeToHex, Transaction.confirmTx, every
signing helper) to reach it was heavier than the helper itself
deserved. Moving it into its own file makes every external
dependency jest-mockable in isolation.

Files:
- src/libs/blockchain/routines/applyGasFeeSeparation.ts (NEW):
  - Exports `applyGasFeeSeparation(tx) -> Promise<{ ok: true } |
    { ok: false; message: string }>`.
  - Same behavior as the inline version: stamps
    transaction_fee.{network_fee, rpc_fee, additional_fee,
    rpc_address}, PROD-only balance check, prepends
    generateFeeDistributionEdits onto tx.content.gcr_edits.
  - Tighter signature — drops the unused `validityData` parameter
    the inline version carried.
  - Re-exports an `ApplyGasFeeSeparationTx` Pick<ITransaction,
    "content" | "hash"> so callers can pass either the SDK
    Transaction or the node-side subclass.

- src/libs/blockchain/routines/validateTransaction.ts:
  - Imports applyGasFeeSeparation from the new module.
  - Inline copy deleted; the fork-gated call site at line 131 now
    invokes the imported helper with `applyGasFeeSeparation(tx)`
    (one-arg).
  - Removed `calculateFeeBreakdown` + `generateFeeDistributionEdits`
    imports — the helper owns them now.

- tests/blockchain/applyGasFeeSeparation.test.ts (NEW): 15 tests
  organised into four describes:
    1. happy path — stamps transaction_fee fields, prepends edits,
       emits 5 edits for 50/50 network + 100% rpc breakdown.
    2. sender address resolution — string passthrough, forgeToHex
       coercion for non-string, error path when forgeToHex throws.
    3. breakdown sanity — rejects NaN, Infinity, fractional, negative
       totals. Drives them via sharedStateStub.networkFee instead of
       mocking the calculateFeeBreakdown module (bun test does not
       auto-isolate module mocks across files; mocking
       calculateCurrentGas here leaked into
       tests/governance/calculateCurrentGas.test.ts during the full
       suite run).
    4. PROD balance check — accept on >=, reject on <, error path
       when GCR.getGCRNativeBalance throws, no balance call in
       non-PROD.

Test suite: 281/282 pass / 715 expect() calls across testing/forks/
+ tests/governance/ + tests/blockchain/. The 1 fail is the
pre-existing snapshotWeightIntegrity Jest-mock setup issue
unrelated to DEM-665.

Typecheck clean except pre-existing L2PS breakage from
stabilisation merge.
…yc#100, DEM-665 P10b)

Promotes scenario 10 from documented placeholder to a full devnet
drive by adding the missing tx-signing infrastructure to the
rehearsal harness. Scenario 10 now submits a real signed tx with a
malicious remove-from-burn GCREdit and asserts the consensus-critical
invariant (burn balance UNCHANGED on every node) end-to-end.

Result on 4-node Postgres devnet: PASS in 126.4s.

Files:
- testing/forks/rehearsal/lib/signing.ts (NEW):
  - generateHarnessKeypair() — fresh ed25519 keypair via
    Cryptography.newFromSeed(crypto.randomBytes(32)). Same primitive
    ucrypto.generateIdentity uses; derivation is bit-identical to a
    production node start. In-memory only, never persisted, devnet
    only.
  - signHarnessTx(kp, content, blockHeight) — runs the fork-aware
    serializeTransactionContent + sha256 + Cryptography.sign chain to
    produce {hash, signature} in the shape the validating node
    expects via manageExecution({extra: "confirmTx", data: tx}).

- testing/forks/rehearsal/lib/devnetControl.ts:
  - stageGenesisWithFundedAccount(fixture, pubkey, balance) — clones
    a rehearsal fixture, appends [pubkey, balance] to its balances
    array, writes the result to data/genesis.json. Reuses the same
    one-time prod-backup stageGenesis() produces.

- testing/forks/rehearsal/scenarios/10-burn-spend-rejection.ts
  (rewritten from placeholder):
  - Generates fresh harness keypair.
  - Injects funded entry into genesis-fork-low-gasFee.json balances.
  - Boots 4-node devnet, waits for fork crossing (height >= 6).
  - Verifies osDenomination.activated + gasFeeSeparation.applied_at_block
    converged.
  - Builds a "send" tx whose gcr_edits include the malicious
    remove-from-burn entry alongside legitimate sender-remove /
    recipient-add.
  - Signs with harness keypair via the fork-aware serializer.
  - POSTs to node-1 as a confirmTx bundle.
  - Sleeps 15s, asserts burn balance == "0" on every node +
    fork_state unchanged.

- decimal_planning/REHEARSAL_RESULTS.md: Run 7 section appended with
  the full harness procedure + result.

Why "burn balance unchanged" instead of "validator returned specific
message": the rejection mechanism can fire either at confirm-time
(validating node refuses to sign ValidityData) OR at apply-time
(GCRBalanceRoutines.apply returns success:false). Both outcomes
produce the same observable state across nodes. The consensus-
meaningful invariant is balance preservation; the specific message
("Cannot deduct from burn address") is a diagnostic log, not part of
the wire response. Unit suite covers the apply-layer guard message
explicitly (tests/blockchain/GCRBalanceRoutines.test.ts, 8 cases).

Test suite (unit): unchanged, 281/282 pass with the pre-existing
snapshotWeightIntegrity fail. Typecheck clean except pre-existing
L2PS breakage.

Reproduction:
    POSTGRES_HOST_PORT=5532 POSTGRES_USER=demosuser POSTGRES_PASSWORD=demospass \\
    bun run testing/forks/rehearsal/scenarios/10-burn-spend-rejection.ts

P10b is fully closed.
@qodo-code-review
Copy link
Copy Markdown
Contributor

ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

Warning

Rate limit exceeded

@tcsenpai has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 24 minutes and 14 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0422d032-5465-48f5-847f-9823506c60b9

📥 Commits

Reviewing files that changed from the base of the PR and between 0381b81 and 7ee15b1.

📒 Files selected for processing (1)
  • src/errors/handleError.ts

Walkthrough

Adds DEM-665 "gasFeeSeparation": new fork config/loader, migration to create burn/treasury accounts, transaction rpc_address field, per-component fee calculation and deterministic GCR edits, burn-address spend prevention, governance safety checks for distribution percentages, extensive tests and rehearsal scenarios, and operational runbooks.

Changes

Fork configuration, loader, burn address

Layer / File(s) Summary
Burn address and fork type model
src/forks/burnAddress.ts, src/forks/forkConfig.ts, src/forks/index.ts
Introduce canonical BURN_ADDRESS, add gasFeeSeparation to ForkName, change fork config to discriminated-union with GasFeeSeparationConfig (treasuryAddress), add PLACEHOLDER_TREASURY_ADDRESS, update defaults and clone helper.
Genesis loader & validation
src/forks/loadForkConfig.ts
Validate gasFeeSeparation.treasuryAddress with strict lowercase hex regex, refuse placeholder when scheduled, unify per-fork validation/write dispatcher, ensure primeFeeDistributionFromForkConfig() runs even if genesis forks block missing, and prime burn/treasury addresses while preserving percentage groups.
Migration exports / re-exports
src/forks/index.ts, src/forks/serializerGate.ts
Re-export new fork types/constants and include rpc_address in post-/pre-fork serialization passthroughs.

Migration & activation wiring

Layer / File(s) Summary
Migration: create burn/treasury accounts & fork_state upsert
src/forks/migrations/gasFeeSeparation.ts
New migration module validating treasury, idempotently creating zero-balance burn and treasury GCR accounts (ensureZeroAccount), upserting fork_state with applied/applied_at_block, exporting FORK_NAME, BURN_ADDRESS, isGasFeeSeparationMigrationApplied, and runGasFeeSeparationMigration.
Block insertion hook
src/libs/blockchain/chainBlocks.ts
Run runGasFeeSeparationMigration inside the same DB transaction during insertBlock when fork machinery enabled and fork becomes active; guarded by idempotency check.
Genesis op alignment
src/libs/blockchain/chainGenesis.ts
Include rpc_address: null in genesis operation fees to align shape with TxFee interface.

Transaction schema, wiring, and persistence

Layer / File(s) Summary
DB + entity + migration
src/migrations/AddRpcAddressToTransactions.ts, src/model/entities/Transactions.ts
Add nullable rpcAddress varchar column to transactions table and mapping property on Transactions entity; migration up/down provided.
Transaction wire mapping and constructors
src/libs/blockchain/transaction.ts, src/libs/l2ps/L2PSBatchAggregator.ts, src/libs/utils/demostdlib/deriveMempoolOperation.ts, src/features/InstantMessagingProtocol/signalingServer/signalingServer.ts
Default transaction_fee.rpc_address initialized to null; toRawTransaction/fromRawTransaction map rpcAddressrpc_address; update various transaction creation paths to emit rpc_address: null.

Fee calculation, distribution, and application

Layer / File(s) Summary
Fee breakdown API
src/libs/blockchain/routines/calculateCurrentGas.ts
Add FeeBreakdown and calculateFeeBreakdown(payload) returning network_fee, rpc_fee, additional_fee, and total; keep legacy composed-gas wrapper for pre-fork compatibility.
Fee distribution edit generators
src/libs/blockchain/gcr/gcr_routines/feeDistribution.ts
New deterministic utilities: generateFeeDistributionEdits and generateSpecialOpsFeeEdits that floor-split components into burn/treasury/(rpc) edits, route rpc-share to treasury when missing, preserve exact totals by routing remainder to treasury, and defensively return [] when runtime fee distribution unprimed.
Validation-time application hook
src/libs/blockchain/routines/applyGasFeeSeparation.ts, src/libs/blockchain/routines/validateTransaction.ts
applyGasFeeSeparation(tx) stamps transaction_fee (including node rpc_address), validates fee breakdown, optionally checks sender native balance in PROD, generates and prepends fee-distribution GCR edits, and is invoked fork-conditionally in confirmTransaction before signing; refuses transactions when fees >0 but no edits generated.
TLSN / native operations
src/libs/blockchain/gcr/gcr_routines/handleNativeOperations.ts
Replace fixed TLSN burn-only fees with fork-gated getTlsnFees(blockHeight) and post-fork use of generateSpecialOpsFeeEdits; pre-fork retains single burn remove edit.
GCR apply guard
src/libs/blockchain/gcr/gcr_routines/GCRBalanceRoutines.ts
Block non-rollback remove operations against configured feeDistribution.burnAddress when fork active (case-insensitive), gated by lastBlockNumber and presence of active fork.

Governance safety bounds

Layer / File(s) Summary
Governance constants & numeric bounds
src/features/networkUpgrade/constants.ts
Add DISTRIBUTION_MAX_CHANGE_PERCENT = 10, DISTRIBUTION_KEYS set; include additionalFee and distribution percent keys in PHASE_1_GOVERNABLE_KEYS, populate HARDCODED_FALLBACK_NETWORK_PARAMETERS with defaults, and add numeric bounds (0–100) for percentage keys.
Safety validations
src/features/networkUpgrade/safetyBounds.ts
Add distribution-group sum-to-100 invariant check on merged (current⊕proposed) views; parameterized per-key percent-cap enforcement (cap argument) using DISTRIBUTION_MAX_CHANGE_PERCENT for distribution keys and legacy cap for others; return first failing key with diagnostics.

Shared runtime state

Layer / File(s) Summary
SharedState shape and priming
src/utilities/sharedState.ts, src/libs/blockchain/routines/loadNetworkParameters.ts
Add additionalFee scalar and `feeDistribution: FeeDistributionRuntime

Tests, rehearsal harness, and operational docs

Layer / File(s) Summary
Unit tests
tests/blockchain/*.test.ts, tests/governance/*.test.ts
Add/extend suites: deterministic fee-split behavior, special-ops splitting, burn-address spend-prevention, applyGasFeeSeparation validation and PROD-balance checks, safety-bounds/governance invariants, and migration tests for idempotency/account creation.
Fork tests & fixtures
testing/forks/*, testing/forks/rehearsal/*
Update many fork tests to use ForkConfigByName and include transaction_fee.rpc_address: null in fixtures; add genesis-fork-low-gasFee.json, rehearsal signing helper, genesis staging helper, node queries for fork_state/gcr, and two rehearsal scenarios (09 fee-distribution, 10 burn-spend-rejection).
Runbooks & plan docs
forking/RUNBOOK_FORK_ACTIVATION.md, forking/gas_separation/PLAN.md, forking/REHEARSAL_RESULTS.md
Add validator-focused runbook, detailed gas-fee-separation implementation plan, and rehearsal results documenting scenarios and harness fixes.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Node as Node / confirmTransaction
    participant Calc as calculateFeeBreakdown
    participant Apply as applyGasFeeSeparation
    participant Gen as generateFeeDistributionEdits
    participant GCR as GCRBalanceApply
    participant DB as DB (fork_state / gcr_main)

    Client->>Node: submit(tx)
    Node->>Node: isForkActive(gasFeeSeparation)?
    alt fork active
        Node->>Calc: calculateFeeBreakdown(tx)
        Calc-->>Node: {network_fee,rpc_fee,additional_fee,total}
        Node->>Apply: applyGasFeeSeparation(tx)
        Apply->>Gen: generateFeeDistributionEdits(sender, fees, rpc_addr)
        Gen-->>Apply: [gcr_edits...]
        Apply->>Node: prepend edits to tx.content.gcr_edits
        Node->>Node: sign validity (includes edits)
    else fork inactive
        Node->>Node: legacy composed gas path
    end
    Node->>DB: insertBlock(tx, edits, ...)
    DB->>GCR: apply(edit)*
    GCR-->>DB: update balances (persist)
Loading
sequenceDiagram
    participant Loader as loadForkConfigFromGenesis
    participant Validator as validateForkEntry
    participant State as SharedState
    participant Block as insertBlock
    participant Migr as runGasFeeSeparationMigration
    participant DB as gcr_main / fork_state

    Loader->>Validator: validate gasFeeSeparation payload
    Validator-->>Loader: validated config (treasuryAddress checked)
    Loader->>State: set forkConfig and prime burn/treasury addresses
    Note right of Block: On activation block
    Block->>Migr: isGasFeeSeparationMigrationApplied?
    Migr-->>Block: not applied
    Block->>Migr: runGasFeeSeparationMigration(tx, blockNumber, treasury)
    Migr->>DB: ensureZeroAccount(BURN_ADDRESS)
    Migr->>DB: ensureZeroAccount(treasuryAddress)
    Migr->>DB: upsert fork_state (applied=true, applied_at_block)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

  • Gas Fee Separation Plan #786: Implements DEM-665 gasFeeSeparation (matches this PR's fork, rpc_address, fee-distribution, migration, burn-guard, and governance changes).

Possibly related PRs

  • kynesyslabs/node#812: Related fork-activation machinery and fork-state integration; overlaps with adding gasFeeSeparation hook and fork config changes.
  • kynesyslabs/node#778: Related governance/network-parameters changes; overlaps with distribution parameter handling and bounds.
  • kynesyslabs/node#755: Related GCR balance application refactors; touches same GCR apply paths where burn-guard was added.

Suggested labels

ReviewedByDojoReviews, Review effort 5/5

Suggested reviewers

  • cwilvx

Poem

🐰 I split the gas into three neat streams,
Burn, treasury, and RPC dreams.
Validators hum, migrations run,
Guarded burn stays safe as one.
Hop—fork activated—balance beams.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/gas-fee-separation-aDJK5

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 12, 2026

Greptile Summary

This PR implements DEM-665 gas fee separation as a second hard fork (gasFeeSeparation), splitting the single lump-sum gas deduction into three components (network_fee, rpc_fee, additional_fee) with distinct distribution rules routed to burn, treasury, and rpc-operator. It also resolves all eight issues flagged in prior review rounds (null-rpcAddress fee leak, hardcoded additional_fee, duplicate burn-address constant, all-zero initialization guard, silent fee bypass in applyGasFeeSeparation, and TLSN fee bypasses).

  • Fee separation core: new feeDistribution.ts module generates balance edits; applyGasFeeSeparation.ts wires it into confirmTransaction; both have hard-error guards when the distribution runtime view is not yet primed.
  • Governance: seven distribution-percentage keys added to NetworkParameters with a tighter ±10% relative cap and a cross-group sum-100 invariant enforced in safetyBounds.ts.
  • Burn-address protection: GCRBalanceRoutines rejects remove operations against the burn address post-fork; burnAddress.ts consolidates the constant that was previously duplicated.

Confidence Score: 5/5

Safe to merge; all eight issues flagged in prior review rounds have been correctly resolved, and the new fee-separation path is thoroughly guarded and tested.

Every previously identified concern — the null-rpcAddress fee leak, hardcoded additional_fee, duplicate burn-address constant, allZero initialization guard, silent fee bypass in applyGasFeeSeparation, and both TLSN fee bypasses — is addressed with explicit hard-error guards and 128 new tests across eight suites. The only remaining findings are stylistic: an unused treasuryPct parameter that is only log-visible, two synonymous export names for the same constant, and an inlined regex that duplicates a named constant elsewhere.

No files require special attention. The fee-distribution and migration paths are well-covered by unit tests and two devnet rehearsal scenarios.

Important Files Changed

Filename Overview
src/libs/blockchain/gcr/gcr_routines/feeDistribution.ts New module. Generates GCREditBalance sequences for regular-tx and TLSN (special-ops) fee distribution. All previous P1s addressed: null-rpcAddress folds to treasury, allZero guard refuses edits before loadNetworkParameters runs.
src/libs/blockchain/routines/applyGasFeeSeparation.ts New extracted module. Stamps fee components onto tx, checks sender balance, prepends fee edits. Hard-error guard prevents silent fee bypass during the feeDistribution initialization window.
src/libs/blockchain/gcr/gcr_routines/handleNativeOperations.ts TLSN request/store fee handling is now fork-gated. Both branches add the same hard-error guard as applyGasFeeSeparation. Previous P1 silent bypass issues resolved.
src/forks/loadForkConfig.ts Extended to validate gasFeeSeparation payload. Burn-address constant consolidated to burnAddress.ts via re-export. primeFeeDistributionFromForkConfig preserves existing percentages to handle the loadNetworkParameters-first startup order.
src/forks/migrations/gasFeeSeparation.ts Atomic, idempotent migration creating burn and treasury accounts at activation block. UPSERT pattern inherits sqlite ≥3.24 requirement already present in osDenomination migration.
src/features/networkUpgrade/safetyBounds.ts Added Layer 3 cross-group sum-100 invariant for distribution keys, and tighter ±10% relative cap via parameterized withinPercentCap.
src/forks/burnAddress.ts New leaf module holding the single source of truth for BURN_ADDRESS, eliminating the prior duplicate literals.
src/libs/blockchain/routines/loadNetworkParameters.ts Now mirrors additionalFee onto sharedState and folds all seven distribution-percentage keys into feeDistribution. Preserves existing burn/treasury addresses when overwriting percentages.
src/forks/index.ts Public surface extended for gasFeeSeparation. Exports two synonymous names for the same burn-address constant, creating mild naming confusion.
src/migrations/AddRpcAddressToTransactions.ts New TypeORM migration adding nullable rpcAddress varchar column with IF NOT EXISTS safety guard.

Sequence Diagram

sequenceDiagram
    participant Client
    participant confirmTransaction
    participant applyGasFeeSeparation
    participant calculateFeeBreakdown
    participant generateFeeDistributionEdits
    participant requireFeeDistribution
    participant GCRBalanceRoutines

    Client->>confirmTransaction: submit tx (post-fork)
    confirmTransaction->>applyGasFeeSeparation: applyGasFeeSeparation(tx)
    applyGasFeeSeparation->>calculateFeeBreakdown: calculateFeeBreakdown(tx)
    calculateFeeBreakdown-->>applyGasFeeSeparation: "{network_fee, rpc_fee, additional_fee, total}"
    applyGasFeeSeparation->>applyGasFeeSeparation: stamp tx.transaction_fee fields + rpc_address
    applyGasFeeSeparation->>generateFeeDistributionEdits: "generateFeeDistributionEdits({senderAddress, rpcAddress, ...})"
    generateFeeDistributionEdits->>requireFeeDistribution: check feeDistribution primed and non-zero
    alt not primed or allZero
        requireFeeDistribution-->>generateFeeDistributionEdits: null - return []
        generateFeeDistributionEdits-->>applyGasFeeSeparation: []
        applyGasFeeSeparation-->>confirmTransaction: "{ok:false, fee distribution not primed}"
    else primed
        requireFeeDistribution-->>generateFeeDistributionEdits: FeeDistributionRuntime
        generateFeeDistributionEdits-->>applyGasFeeSeparation: [remove(sender), add(burn), add(treasury), add(rpcOp)]
        applyGasFeeSeparation->>applyGasFeeSeparation: prepend fee edits onto tx.gcr_edits
        applyGasFeeSeparation-->>confirmTransaction: "{ok:true}"
        confirmTransaction->>GCRBalanceRoutines: apply gcr_edits
        GCRBalanceRoutines->>GCRBalanceRoutines: reject remove from burnAddress (post-fork, non-rollback)
    end
Loading

Reviews (8): Last reviewed commit: "fix(handleError): prefer cause.toString(..." | Re-trigger Greptile

Comment thread src/libs/blockchain/gcr/gcr_routines/feeDistribution.ts
Comment thread src/libs/blockchain/routines/calculateCurrentGas.ts Outdated
Comment thread src/forks/loadForkConfig.ts Outdated
Comment thread src/forks/loadForkConfig.ts
Comment thread src/forks/migrations/gasFeeSeparation.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (7)
docs/GAS_FEE_SEPARATION_PLAN.md (1)

83-107: 💤 Low value

Consider adding language identifiers to fenced code blocks.

The two diagram blocks (lines 83-91) are missing language identifiers. While these are flowchart-style diagrams rather than executable code, adding text or mermaid identifiers would satisfy markdown linters and improve rendering.

♻️ Proposed fix
-```
+```text
 TX created → single composedGas calculated → single "pay_gas" Operation → single "remove" GCREdit

```diff
-```
+```text
 TX created → 3 fee components calculated separately → rpc_address captured
             → per-component GCREdits generated:
 ...
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @docs/GAS_FEE_SEPARATION_PLAN.md around lines 83 - 107, The two fenced
diagram blocks starting with "TX created → single composedGas calculated →
single "pay_gas" Operation → single "remove" GCREdit" and "TX created → 3 fee
components calculated separately → rpc_address captured" should include a
language identifier (e.g., text or mermaid) so markdown linters/renderers
treat them correctly; update those two triple-backtick blocks to start with
text (or mermaid if you prefer flowchart rendering) while leaving the
diagram contents unchanged.


</details>

</blockquote></details>
<details>
<summary>testing/forks/rehearsal/lib/devnetControl.ts (1)</summary><blockquote>

`242-242`: _💤 Low value_

**Prefer imported `readFileSync` over inline `require("fs")`.**

The file already imports `readFileSync` from `fs` at line 14. Using the imported binding is more consistent with the rest of the file and avoids the inline `require` call.



<details>
<summary>♻️ Proposed fix</summary>

```diff
-    const raw = require("fs").readFileSync(rehearsalGenesisPath, "utf8")
+    const raw = readFileSync(rehearsalGenesisPath, "utf8")
```
</details>

As per coding guidelines, the static analysis hint suggests preferring `node:fs` over `fs`, but the entire file uses bare `fs` imports, so maintaining consistency within this file is more valuable than mixing import styles.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@testing/forks/rehearsal/lib/devnetControl.ts` at line 242, Replace the inline
require("fs").readFileSync call that assigns to raw with the already-imported
readFileSync binding: use readFileSync(rehearsalGenesisPath, "utf8") instead of
require("fs").readFileSync(...). Update the statement that sets raw (using the
rehearsalGenesisPath symbol) so it relies on the module-level imported
readFileSync to keep import style consistent with the rest of the file.
```

</details>

</blockquote></details>
<details>
<summary>testing/forks/rehearsal/lib/signing.ts (1)</summary><blockquote>

`21-21`: _💤 Low value_

**Consider using `node:crypto` prefix for Node.js core module imports.**

This is a style preference for modern Node.js (16+) that makes it explicit when importing from Node.js core vs npm packages. However, the codebase doesn't consistently use the `node:` prefix (e.g., `devnetControl.ts` uses bare `fs` imports), so adopting this would be a broader codebase-wide decision.



<details>
<summary>♻️ Proposed fix</summary>

```diff
-import { randomBytes } from "crypto"
+import { randomBytes } from "node:crypto"
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@testing/forks/rehearsal/lib/signing.ts` at line 21, Update the Node core
import in signing.ts to use the explicit node: prefix: replace the bare import
of randomBytes from "crypto" with the Node-prefixed module so it's clear this is
a core module import (look for the top-level import statement that currently
reads import { randomBytes } from "crypto"). Ensure the rest of the file
(functions using randomBytes) is unchanged and that the new import follows the
project's style (you may leave other files alone to avoid a global style
change).
```

</details>

</blockquote></details>
<details>
<summary>src/libs/blockchain/transaction.ts (1)</summary><blockquote>

`73-77`: _💤 Low value_

**Clarify the "P6" phase reference.**

The comment references "P6" which appears to be an internal phase identifier. For long-term maintainability, consider either expanding this acronym inline or linking to the specification document where phases are defined.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/transaction.ts` around lines 73 - 77, Update the inline
comment above the rpc_address field in transaction.ts to expand or clarify the
"P6" phase reference so future readers understand it without internal context;
modify the comment that currently reads "confirmTransaction (P6)" to either
spell out what P6 stands for (e.g., "Phase 6: validation/confirmation") or add a
short parenthetical pointer to the design/spec (e.g., "confirmTransaction (Phase
6 — validating node; see SPEC_SECTION_X)"), leaving the surrounding explanation
about pre-fork/null behavior unchanged.
```

</details>

</blockquote></details>
<details>
<summary>src/libs/blockchain/routines/validateTransaction.ts (1)</summary><blockquote>

`346-347`: _💤 Low value_

**Clarify the context of this legacy code path.**

The comment mentions "internal gas Operation" but the surrounding code (lines 62-76, 218-353) suggests `defineGas` is largely dead code superseded by the GCREdit system. Consider adding a note that this function is deprecated/legacy to help future maintainers understand why `rpc_address: null` is appropriate here.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/routines/validateTransaction.ts` around lines 346 - 347,
The comment about "internal gas Operation" is unclear—update the legacy note to
state that defineGas (and its surrounding logic in validateTransaction.ts) is a
deprecated/legacy code path largely superseded by the GCREdit system, and
explicitly document why rpc_address is set to null for internal gas operations;
reference the defineGas function and the rpc_address: null line so future
maintainers understand this is intentional for the legacy internal-gas path
rather than a missing RPC routing implementation.
```

</details>

</blockquote></details>
<details>
<summary>testing/forks/rehearsal/scenarios/10-burn-spend-rejection.ts (1)</summary><blockquote>

`297-309`: _⚡ Quick win_

**Prefer condition-based waiting over fixed sleep to reduce flakiness.**

`await sleep(15_000)` can race on slower environments. Use `waitFor` on the post-submission burn-balance invariant (or tx finality signal) instead of a fixed delay.  
 
<details>
<summary>Proposed change</summary>

```diff
-    // Give the network time to settle (any propagation, mempool
-    // sweep, or apply attempt).
-    await sleep(15_000)
-
-    // Consensus-critical invariant: burn balance UNCHANGED on every
-    // node. If the guard is broken, this is where it surfaces.
-    await assertGcrAccountConvergence(
-        NODE_IDS,
-        BURN_ADDRESS,
-        "0",
-        "post-submission burn account",
-    )
+    // Wait until the post-submission invariant is observable.
+    await waitFor(
+        async () => {
+            await assertGcrAccountConvergence(
+                NODE_IDS,
+                BURN_ADDRESS,
+                "0",
+                "post-submission burn account",
+            )
+            return true
+        },
+        {
+            description: "burn balance remains unchanged after malicious submission",
+            timeoutMs: 60_000,
+            intervalMs: 2_000,
+        },
+    )
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@testing/forks/rehearsal/scenarios/10-burn-spend-rejection.ts` around lines
297 - 309, Replace the fixed await sleep(15_000) with a condition-based wait
that polls the post-submission invariant until it succeeds or a timeout elapses:
use the existing waitFor utility (or implement a short-poll loop) to repeatedly
call assertGcrAccountConvergence(NODE_IDS, BURN_ADDRESS, "0", "post-submission
burn account") (catching/asserting failures between polls) at a small interval
(e.g., 500–1000ms) and fail the test if the overall timeout (e.g., 30–60s) is
exceeded; this ensures we wait for the burn-balance invariant (or a tx finality
signal) instead of a fixed 15s sleep and reduces flakiness.
```

</details>

</blockquote></details>
<details>
<summary>src/libs/blockchain/gcr/gcr_routines/feeDistribution.ts (1)</summary><blockquote>

`120-171`: _💤 Low value_

**Consider refactoring to an options object to reduce parameter count.**

The function has 9 parameters, which exceeds typical maintainability limits (commonly 5-7). While the current implementation is correct and the function is private, refactoring to accept a single options object would improve readability and make future parameter additions easier.




<details>
<summary>♻️ Example refactor with options object</summary>

```diff
+interface TwoRecipientSplitOptions {
+    componentName: "network_fee" | "additional_fee"
+    total: number
+    burnPct: number
+    treasuryPct: number
+    burnAddress: string
+    treasuryAddress: string
+    senderAddress: string
+    txHash: string
+    isRollback: boolean
+}
+
 function emitTwoRecipientSplit(
-    componentName: "network_fee" | "additional_fee",
-    total: number,
-    burnPct: number,
-    treasuryPct: number,
-    burnAddress: string,
-    treasuryAddress: string,
-    senderAddress: string,
-    txHash: string,
-    isRollback: boolean,
+    options: TwoRecipientSplitOptions,
 ): GCREditBalance[] {
+    const {
+        componentName,
+        total,
+        burnPct,
+        treasuryPct,
+        burnAddress,
+        treasuryAddress,
+        senderAddress,
+        txHash,
+        isRollback,
+    } = options
     if (total <= 0) return []
     // ... rest remains the same
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/gcr/gcr_routines/feeDistribution.ts` around lines 120 -
171, The function emitTwoRecipientSplit has too many positional parameters;
refactor it to accept a single options object (e.g. { componentName, total,
burnPct, treasuryPct, burnAddress, treasuryAddress, senderAddress, txHash,
isRollback }), destructure those fields at the top of emitTwoRecipientSplit and
keep the existing logic and calls to makeBalanceEdit and log.debug unchanged;
update all callers of emitTwoRecipientSplit to pass the new options object and
adjust any unit tests or usages accordingly so behavior remains identical.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

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 @decimal_planning/RUNBOOK_FORK_ACTIVATION.md:

  • Line 536: The runbook line for "DEM-665 status" is ambiguous about the SDK
    to deploy; replace the existing sentence containing "Source-branch:
    claude/gas-fee-separation-aDJK5. SDK companion: 4.0.0-rc.1 (pending publish;
    user owns)" with a single unambiguous statement that specifies the exact
    required SDK version or an allowed semantic version range (for example "SDK
    companion: 4.0.0" or "SDK companion: >=4.0.0 <4.1.0"), and remove the "(pending
    publish)" phrase unless it is still true; keep the Source-branch identifier only
    if necessary for traceability and ensure the final line reads clearly as the
    authoritative SDK requirement for fork activation.

In @src/forks/forkConfig.ts:

  • Line 42: The empty interface OsDenominationConfig extends BaseForkConfig and
    triggers the no-empty-interface lint rule; replace the interface declaration
    with a type alias (e.g., "type OsDenominationConfig = BaseForkConfig;") so
    OsDenominationConfig is an alias of BaseForkConfig and the lint error is
    resolved, updating the declaration of OsDenominationConfig accordingly while
    keeping BaseForkConfig unchanged.

In @src/libs/blockchain/routines/applyGasFeeSeparation.ts:

  • Around line 80-85: The catch blocks in applyGasFeeSeparation that build
    messages like failed to resolve sender address: ${msg} use String(e) which
    yields "[object Object]" for plain objects; replace that stringification with a
    robust helper (or inline) that if e is an Error uses e.name/e.message, otherwise
    attempts JSON.stringify(e, null, 2) and falls back to util.inspect(e) or
    String(e) on failure, and use that result for both the "failed to resolve sender
    address" and the later similar catch (resolvePaymaster) messages so the returned
    message contains useful serialized details.

Nitpick comments:
In @docs/GAS_FEE_SEPARATION_PLAN.md:

  • Around line 83-107: The two fenced diagram blocks starting with "TX created →
    single composedGas calculated → single "pay_gas" Operation → single "remove"
    GCREdit" and "TX created → 3 fee components calculated separately → rpc_address
    captured" should include a language identifier (e.g., text or mermaid) so
    markdown linters/renderers treat them correctly; update those two
    triple-backtick blocks to start with text (or mermaid if you prefer
    flowchart rendering) while leaving the diagram contents unchanged.

In @src/libs/blockchain/gcr/gcr_routines/feeDistribution.ts:

  • Around line 120-171: The function emitTwoRecipientSplit has too many
    positional parameters; refactor it to accept a single options object (e.g. {
    componentName, total, burnPct, treasuryPct, burnAddress, treasuryAddress,
    senderAddress, txHash, isRollback }), destructure those fields at the top of
    emitTwoRecipientSplit and keep the existing logic and calls to makeBalanceEdit
    and log.debug unchanged; update all callers of emitTwoRecipientSplit to pass the
    new options object and adjust any unit tests or usages accordingly so behavior
    remains identical.

In @src/libs/blockchain/routines/validateTransaction.ts:

  • Around line 346-347: The comment about "internal gas Operation" is
    unclear—update the legacy note to state that defineGas (and its surrounding
    logic in validateTransaction.ts) is a deprecated/legacy code path largely
    superseded by the GCREdit system, and explicitly document why rpc_address is set
    to null for internal gas operations; reference the defineGas function and the
    rpc_address: null line so future maintainers understand this is intentional for
    the legacy internal-gas path rather than a missing RPC routing implementation.

In @src/libs/blockchain/transaction.ts:

  • Around line 73-77: Update the inline comment above the rpc_address field in
    transaction.ts to expand or clarify the "P6" phase reference so future readers
    understand it without internal context; modify the comment that currently reads
    "confirmTransaction (P6)" to either spell out what P6 stands for (e.g., "Phase
    6: validation/confirmation") or add a short parenthetical pointer to the
    design/spec (e.g., "confirmTransaction (Phase 6 — validating node; see
    SPEC_SECTION_X)"), leaving the surrounding explanation about pre-fork/null
    behavior unchanged.

In @testing/forks/rehearsal/lib/devnetControl.ts:

  • Line 242: Replace the inline require("fs").readFileSync call that assigns to
    raw with the already-imported readFileSync binding: use
    readFileSync(rehearsalGenesisPath, "utf8") instead of
    require("fs").readFileSync(...). Update the statement that sets raw (using the
    rehearsalGenesisPath symbol) so it relies on the module-level imported
    readFileSync to keep import style consistent with the rest of the file.

In @testing/forks/rehearsal/lib/signing.ts:

  • Line 21: Update the Node core import in signing.ts to use the explicit node:
    prefix: replace the bare import of randomBytes from "crypto" with the
    Node-prefixed module so it's clear this is a core module import (look for the
    top-level import statement that currently reads import { randomBytes } from
    "crypto"). Ensure the rest of the file (functions using randomBytes) is
    unchanged and that the new import follows the project's style (you may leave
    other files alone to avoid a global style change).

In @testing/forks/rehearsal/scenarios/10-burn-spend-rejection.ts:

  • Around line 297-309: Replace the fixed await sleep(15_000) with a
    condition-based wait that polls the post-submission invariant until it succeeds
    or a timeout elapses: use the existing waitFor utility (or implement a
    short-poll loop) to repeatedly call assertGcrAccountConvergence(NODE_IDS,
    BURN_ADDRESS, "0", "post-submission burn account") (catching/asserting failures
    between polls) at a small interval (e.g., 500–1000ms) and fail the test if the
    overall timeout (e.g., 30–60s) is exceeded; this ensures we wait for the
    burn-balance invariant (or a tx finality signal) instead of a fixed 15s sleep
    and reduces flakiness.

</details>

<details>
<summary>🪄 Autofix (Beta)</summary>

Fix all unresolved CodeRabbit comments on this PR:

- [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended)
- [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: Organization UI

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `3c906360-ae9d-4a3c-b0b8-0b569448d559`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 82b58418bb84d4c56e6afedb2c591bd484c92d4f and bcaec89601bcf14475eaaa6e6172343ef3c364c3.

</details>

<details>
<summary>📒 Files selected for processing (52)</summary>

* `decimal_planning/REHEARSAL_RESULTS.md`
* `decimal_planning/RUNBOOK_FORK_ACTIVATION.md`
* `docs/GAS_FEE_SEPARATION_PLAN.md`
* `package.json`
* `src/features/InstantMessagingProtocol/signalingServer/signalingServer.ts`
* `src/features/networkUpgrade/constants.ts`
* `src/features/networkUpgrade/safetyBounds.ts`
* `src/forks/forkConfig.ts`
* `src/forks/index.ts`
* `src/forks/loadForkConfig.ts`
* `src/forks/migrations/gasFeeSeparation.ts`
* `src/forks/serializerGate.ts`
* `src/libs/blockchain/chainBlocks.ts`
* `src/libs/blockchain/chainGenesis.ts`
* `src/libs/blockchain/gcr/gcr_routines/GCRBalanceRoutines.ts`
* `src/libs/blockchain/gcr/gcr_routines/feeDistribution.ts`
* `src/libs/blockchain/gcr/gcr_routines/handleNativeOperations.ts`
* `src/libs/blockchain/routines/applyGasFeeSeparation.ts`
* `src/libs/blockchain/routines/calculateCurrentGas.ts`
* `src/libs/blockchain/routines/loadNetworkParameters.ts`
* `src/libs/blockchain/routines/validateTransaction.ts`
* `src/libs/blockchain/transaction.ts`
* `src/libs/l2ps/L2PSBatchAggregator.ts`
* `src/libs/utils/demostdlib/deriveMempoolOperation.ts`
* `src/migrations/AddRpcAddressToTransactions.ts`
* `src/model/entities/Transactions.ts`
* `src/utilities/sharedState.ts`
* `testing/forks/amountCanonical.test.ts`
* `testing/forks/disableForkMachineryFlag.test.ts`
* `testing/forks/forkBoundary.test.ts`
* `testing/forks/forkGates.test.ts`
* `testing/forks/getNetworkInfo.test.ts`
* `testing/forks/integration.test.ts`
* `testing/forks/loadForkConfig.test.ts`
* `testing/forks/migrations/gasFeeSeparation.test.ts`
* `testing/forks/migrations/genesisFailLoud.test.ts`
* `testing/forks/postForkSerializer.test.ts`
* `testing/forks/rehearsal/genesis/genesis-fork-low-gasFee.json`
* `testing/forks/rehearsal/lib/assertions.ts`
* `testing/forks/rehearsal/lib/devnetControl.ts`
* `testing/forks/rehearsal/lib/nodeQueries.ts`
* `testing/forks/rehearsal/lib/signing.ts`
* `testing/forks/rehearsal/run-all.sh`
* `testing/forks/rehearsal/scenarios/09-fee-distribution.ts`
* `testing/forks/rehearsal/scenarios/10-burn-spend-rejection.ts`
* `testing/forks/serializerGate.test.ts`
* `tests/blockchain/GCRBalanceRoutines.test.ts`
* `tests/blockchain/applyGasFeeSeparation.test.ts`
* `tests/blockchain/feeDistribution.test.ts`
* `tests/blockchain/handleNativeOperations.test.ts`
* `tests/governance/calculateCurrentGas.test.ts`
* `tests/governance/safetyBounds.test.ts`

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread decimal_planning/RUNBOOK_FORK_ACTIVATION.md Outdated
Comment thread src/forks/forkConfig.ts Outdated
Comment thread src/libs/blockchain/routines/applyGasFeeSeparation.ts
…all-10 green (DEM-665)

Two pre-existing harness bugs surfaced during the full run-all.sh
acceptance gate for the DEM-665 branch. Fixed both; rehearsal cycle
now ends 10/10 PASS in 1929s.

Bug 1 — run-all.sh empty-array expansion under macOS bash 3.2:
  set -u + "${EMPTY_ARRAY[@]}" throws "unbound variable" on bash
  3.2. The runner crashed on the first scenario before bun ever
  fired. Switched to the portable
  ${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"} parameter expansion which only
  emits words when the array is non-empty.

Bug 2 — rpcNodeCall wire shape (lib/nodeQueries.ts):
  The harness flattened extraParams alongside message
  (params: [{ message, ...extraParams }]). manageNodeCall unpacks
  content.data and forwards it to every handler; handlers expect
  data.address (or similar). The flattened shape left
  data === undefined, every parametric nodeCall returned
  "Error in nodeCall: TypeError: undefined is not an object".

  Pre-myc#86 the bug was invisible because cross-node assertions
  compared Set.size === 1 of all-null returns and passed trivially.
  myc#86 strictened the null-check and scenario 06 started failing
  on every run.

  Fix: wrap extras under data:

      params: [{
          message,
          data: Object.keys(extraParams).length > 0
              ? extraParams
              : {},
      }]

  Parameter-free RPCs (getLastBlockNumber, getNetworkInfo, ...) are
  unaffected because their handlers never read data.

Run 8 results appended to decimal_planning/REHEARSAL_RESULTS.md:
  PASS  04-genesis-hash-invariance        93s
  PASS  01-all-cross-fork                168s
  PASS  07-sum-invariant-audit           161s
  PASS  08-idempotent-restart            120s
  PASS  05-cap-policy-fires-loud         241s
  PASS  06-mid-flight-tx                 248s
  PASS  02-validator-desync-recovery     169s
  PASS  03-fresh-node-post-fork          434s
  PASS  09-fee-distribution              166s
  PASS  10-burn-spend-rejection          129s
  Total: 1929s wall-clock.

Side note for ops: a long rehearsal cycle can corrupt the Docker
buildkit snapshot graph
("parent snapshot ... does not exist" extraction failures).
`docker system prune -a -f --volumes` rebuilds it cleanly. Document
as a between-cycle housekeeping step.

DEM-665 implementation is gated clean against the full rehearsal
cycle. Branch ready for review.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
decimal_planning/REHEARSAL_RESULTS.md (1)

695-706: ⚡ Quick win

Add language specifier to fenced code block.

The fenced code block starting at line 695 should specify a language for proper syntax highlighting and markdown linting compliance.

📝 Proposed fix
-```
+```text
 PASS  04-genesis-hash-invariance      93s
 PASS  01-all-cross-fork              168s
 PASS  07-sum-invariant-audit         161s
🤖 Prompt for 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.

In `@decimal_planning/REHEARSAL_RESULTS.md` around lines 695 - 706, The fenced
code block that lists the PASS lines (e.g., "PASS  04-genesis-hash-invariance
93s" etc.) should include a language specifier for markdown linting and
highlighting; update the opening fence from ``` to ```text (or another
appropriate language) so the block becomes ```text followed by the existing
lines and the closing ```.
🤖 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.

Nitpick comments:
In `@decimal_planning/REHEARSAL_RESULTS.md`:
- Around line 695-706: The fenced code block that lists the PASS lines (e.g.,
"PASS  04-genesis-hash-invariance 93s" etc.) should include a language specifier
for markdown linting and highlighting; update the opening fence from ``` to
```text (or another appropriate language) so the block becomes ```text followed
by the existing lines and the closing ```.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 63f6ea61-fe4a-4830-ab0e-864fd43976ab

📥 Commits

Reviewing files that changed from the base of the PR and between bcaec89 and 94d379d.

📒 Files selected for processing (3)
  • decimal_planning/REHEARSAL_RESULTS.md
  • testing/forks/rehearsal/lib/nodeQueries.ts
  • testing/forks/rehearsal/run-all.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • testing/forks/rehearsal/run-all.sh

tcsenpai added 2 commits May 12, 2026 17:19
Greptile review of PR #817 surfaced 3 P1 + 2 P2 findings. All
addressed:

P1 — feeDistribution.ts rpc_fee silent leak (line 250)
  Previous code dropped the entire rpc_fee block when rpcAddress
  was null — no remove from sender, no add to anyone, sender
  silently kept the tokens. Inconsistent with
  generateSpecialOpsFeeEdits which folds rpc share into treasury.
  Fix: sender's remove ALWAYS fires; rpc share folds into treasury
  when no rpc operator is identified. Updated the relevant unit
  test to assert sender-removed + treasury-credited + sum
  invariant.

P1 — calculateCurrentGas.ts additional_fee ignored governance
  (line 81)
  additional_fee was hardcoded to 0 while the additionalFee key
  was wired into PHASE_1_GOVERNABLE_KEYS + safetyBounds + the SDK
  NetworkParameters shape. A passing governance proposal raising
  it would have been stored but invisible to the collection path
  — silent fee-leak surface.
  Fix: added additionalFee scalar to SharedState (default 0),
  loadNetworkParameters mirrors NetworkParameters.additionalFee
  onto the scalar, calculateFeeBreakdown reads from shared state.
  New unit test asserts governance-driven additionalFee actually
  surfaces in the breakdown.

P1 — loadForkConfig.ts BURN_ADDRESS duplicate constant (line 46)
  loadForkConfig.ts and migrations/gasFeeSeparation.ts both
  declared their own "0x" + "0".repeat(64) literal, with a unit
  test guarding equality. If the test were skipped (or the
  pre-existing failure masked it) the two could drift — migration
  creates the burn account at one literal while the runtime guard
  reads from another, allowing burned coins to be re-circulated.
  Fix: new leaf module src/forks/burnAddress.ts owns the single
  source of truth. Both modules import from it and re-export
  under their previous names so call sites stay stable.
  Invariant is now compile-time.

P2 — feeDistribution.ts zero-percent init window (line 258)
  loadForkConfigFromGenesis primes feeDistribution with
  percentages = 0 to keep the structure non-null before
  loadNetworkParameters folds governance values. A post-fork tx
  processed in that window would route 100% of fees to treasury
  invisibly because all burn/rpc shares would be 0.
  Fix: requireFeeDistribution now refuses to emit edits when all
  seven percentage fields are zero, with a clear log line. The
  caller (applyGasFeeSeparation) surfaces the rejection through
  the standard ValidityData failure path.

P2 — gasFeeSeparation.ts sqlite ON CONFLICT portability (line 326)
  ON CONFLICT (col) DO UPDATE SET … EXCLUDED.* requires sqlite
  ≥ 3.24.0. Documented as a portability note in-code; verified
  Bun's bundled sqlite is 3.44.2 (well above floor). The
  osDenomination migration already uses the same syntax, so the
  floor is implicit in the rehearsal stack; the doc is for future
  image downgrades.

Test suite: 282/283 pass (1 pre-existing unrelated fail). Unit
suites for fee-distribution, calculateFeeBreakdown, and
applyGasFeeSeparation all green with the updated semantics.
Typecheck clean except pre-existing L2PS breakage.

Files:
- src/forks/burnAddress.ts (NEW): single source of truth.
- src/forks/loadForkConfig.ts: re-export from leaf.
- src/forks/migrations/gasFeeSeparation.ts: re-export + sqlite note.
- src/libs/blockchain/gcr/gcr_routines/feeDistribution.ts:
  rpc-fold-to-treasury + zero-percent guard.
- src/libs/blockchain/routines/calculateCurrentGas.ts:
  additional_fee from sharedState.
- src/libs/blockchain/routines/loadNetworkParameters.ts:
  mirror additionalFee onto sharedState.
- src/utilities/sharedState.ts: additionalFee field.
- tests/blockchain/feeDistribution.test.ts: null-rpc test updated.
- tests/blockchain/applyGasFeeSeparation.test.ts: stub adds
  additionalFee.
- tests/governance/calculateCurrentGas.test.ts: stub adds
  additionalFee + new governance-mutability test.
Three additional findings from CodeRabbit's review pass, all
addressed:

#1 — RUNBOOK_FORK_ACTIVATION.md:536 SDK version ambiguity
  Final line said "SDK companion: 4.0.0-rc.1 (pending publish; user
  owns)". The PR has since shipped the 3.1.0 → 4.0.0 pin bump
  (commit b8e9bff) so the rc-1 / pending-publish wording is now
  stale and could mislead validators about which version to deploy.
  Fix: changed to the unambiguous
  "**`@kynesyslabs/demosdk@4.0.0`** (required at and after fork
  activation; pinned in `node/package.json`)" form.

#2 — forkConfig.ts:42 empty interface
  `interface OsDenominationConfig extends BaseForkConfig {}` trips
  `@typescript-eslint/no-empty-object-type`. The type carries no
  additional members; an empty interface adds no information over a
  plain alias.
  Fix: switched to `export type OsDenominationConfig = BaseForkConfig`
  with a comment explaining the why (CodeRabbit feedback + the
  consumer-narrows-via-ForkName invariant). Structurally identical;
  zero runtime impact.

#3 — applyGasFeeSeparation.ts:81 + 120 String(e) on non-Error
  Both catch blocks did `e instanceof Error ? e.message : String(e)`.
  `String(plainObject)` collapses to "[object Object]" — the
  structured shape vanishes when something other than an Error gets
  thrown (e.g. a raw RPC body). SonarCloud also flagged this.
  Fix: new `stringifyNonError(e)` helper that JSON.stringify's the
  value, falling back to `String(e)` if serialisation itself throws
  (cyclic graph, BigInt without rawJSON). The diagnostic path never
  re-throws.

Test suite: 282/283 pass (1 pre-existing unrelated fail).
Typecheck clean except pre-existing L2PS breakage.
Comment thread src/libs/blockchain/routines/applyGasFeeSeparation.ts
tcsenpai added 2 commits May 12, 2026 17:42
Single coherent operator-facing runbook for the combined fork
(osDenomination + gasFeeSeparation), one folder per design spec.
14 stale planning docs removed.

New layout:

  forking/
    RUNBOOK_FORK_ACTIVATION.md      ← combined runbook (new home, was
                                      decimal_planning/RUNBOOK_…)
    REHEARSAL_RESULTS.md             ← 8 rehearsal-run audit trail
    decimal_planning/
      SPEC.md                        ← DEM→OS denomination design rationale
    gas_separation/
      PLAN.md                        ← as-shipped DEM-665 plan
                                      (was docs/GAS_FEE_SEPARATION_PLAN.md)

The runbook is rewritten as a single 10-section flow rather than the
prior "decimals body + DEM-665 appendix" structure. Treats the
combined fork as one event:
  §0  TL;DR
  §1  one-screen architecture
  §2  pre-flight checklist (covers both forks)
  §3  height selection + seal genesis
  §4  fork-day timeline with the combined log sequence
       (osDenomination FIRST then gasFeeSeparation, ordered in
       chainBlocks)
  §5  "what right looks like" reference values
  §6  recovery procedures
  §7  post-fork operational notes
  §8  governance — mutable distribution percentages
  §9  don't-do list (combined)
  §10 references — file:line map for code + design docs

Deleted (superseded):
  decimal_planning/IDEA.md                   — initial brainstorm
  decimal_planning/SPEC_P4.md                — SDK 3.0.0-rc.1 phase
  decimal_planning/forking_feasibility.md    — pre-impl feasibility
  decimal_planning/NEXT_STEPS.md             — phase planning
  decimal_planning/PAUSED.md                 — wait-state note
  decimal_planning/PR_REVIEW_DISMISSED.md    — per-PR notes
  decimal_planning/PR_REVIEW_DISMISSED_812.md
  decimal_planning/audit_node.md             — pre-impl audits
  decimal_planning/audit_node_post_staking.md
  decimal_planning/audit_sdk.md
  decimal_planning/surface_scan.md           — early code-scan
  decimal_planning/LOG.md                    — session logbook (git history)
  decimal_planning/DEVNET_READINESS.md       — one-shot pre-rehearsal audit
  decimal_planning/REHEARSAL_PLAN.md         — rehearsal design (harness
                                                IS the plan now)
  decimal_planning/                          — empty directory removed

In-tree references updated to point at the new paths:
  src/config/{types,defaults}.ts
  src/forks/amountCanonical.ts
  testing/forks/{forkGates.test.ts,preflight.ts}
  testing/forks/rehearsal/README.md
  forking/RUNBOOK_FORK_ACTIVATION.md (self)
  forking/REHEARSAL_RESULTS.md
  forking/gas_separation/PLAN.md

Test suite: 282/283 pass (1 pre-existing unrelated fail). Typecheck
clean except pre-existing L2PS breakage.
…dits emitted (Greptile P1 round 3)

Greptile flagged a silent fee-bypass path on the latest review pass:

  generateFeeDistributionEdits returns [] when
  requireFeeDistribution() returns null — either feeDistribution is
  itself null OR every percentage is still 0 (the transient init
  window between loadForkConfigFromGenesis and loadNetworkParameters).
  applyGasFeeSeparation was prepending the empty array onto
  gcr_edits and returning { ok: true } in that case. The sender's
  balance was checked against breakdown.total but no deduction
  edits were emitted: the total fee was silently forgiven while
  the transaction was accepted.

The justification comment in feeDistribution.ts claimed
"the caller surfaces the rejection through applyGasFeeSeparation's
failure path" — the actual code path was silent success.

Fix: applyGasFeeSeparation now refuses the tx whenever
breakdown.total > 0 AND generateFeeDistributionEdits returned 0
edits. Returns { ok: false, message: "fee distribution not primed
— refusing to accept post-fork tx without fee collection" }. The
ValidityData failure path signs that message into the response,
matching the original intent.

Zero-total txs still pass — the no-edits return is correct in that
case.

Tests: 3 new cases in tests/blockchain/applyGasFeeSeparation.test.ts:
  - rejects post-fork tx when feeDistribution is null
  - rejects post-fork tx when all percentages are 0 (init window)
  - accepts zero-total tx even when feeDistribution returns no edits

beforeEach/afterEach were widened to restore feeDistribution to the
SPEC defaults between cases so a sibling case can mutate it without
leaking state to the PROD-balance-check block.

Test suite: 282/283 pass (1 pre-existing snapshotWeightIntegrity
fail unrelated). 18/18 in tests/blockchain/applyGasFeeSeparation.test.ts.
Comment thread src/libs/blockchain/gcr/gcr_routines/handleNativeOperations.ts
Comment thread src/libs/blockchain/gcr/gcr_routines/handleNativeOperations.ts
[UNKNOWN_ERROR] log lines previously dropped the underlying cause,
leaving operators with a one-line "Connection terminated unexpectedly"
or an empty AggregateError message and no stack — boot failures looked
like the node had no reason to fall over. The cause is forwarded into
AppError by normalizeError but logError never read it.

formatCauseChain walks `error.cause` (Node 16+ chained cause) AND
`error.errors[]` (AggregateError siblings — happy-eyeballs IPv4/IPv6
connect failures and parallel-promise rejections live here). Depth and
sibling count are bounded so a cyclical or pathological chain can't
flood the logger.

This surfaced the underlying pg "Connection terminated unexpectedly"
during a stale-data-dir boot — exactly the kind of diagnostic the
operator should see without attaching a debugger.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/errors/handleError.ts (1)

223-264: 💤 Low value

Reduce cognitive complexity by extracting AggregateError handling.

SonarCloud flags this function at cognitive complexity 16 (limit 15). The Array.isArray(siblings) branch with its nested loop and overflow message is the bulk of the score — pulling it into a small helper drops the complexity below the threshold without changing behavior.

♻️ Suggested refactor
+function formatAggregateSiblings(
+    siblings: unknown[],
+    indent: string,
+    depth: number,
+): string {
+    const shown = siblings.slice(0, 5)
+    let out = ""
+    for (let i = 0; i < shown.length; i++) {
+        out += `\n${indent}[error ${i + 1}/${siblings.length}]`
+        out += formatCauseChain(shown[i], depth + 1)
+    }
+    if (siblings.length > shown.length) {
+        out += `\n${indent}... ${siblings.length - shown.length} more`
+    }
+    return out
+}
+
 function formatCauseChain(cause: unknown, depth = 0): string {
     ...
         const siblings = (cause as { errors?: unknown }).errors
         if (Array.isArray(siblings) && siblings.length > 0) {
-            const shown = siblings.slice(0, 5)
-            for (let i = 0; i < shown.length; i++) {
-                out += `\n${indent}[error ${i + 1}/${siblings.length}]`
-                out += formatCauseChain(shown[i], depth + 1)
-            }
-            if (siblings.length > shown.length) {
-                out += `\n${indent}... ${siblings.length - shown.length} more`
-            }
+            out += formatAggregateSiblings(siblings, indent, depth)
         }
🤖 Prompt for 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.

In `@src/errors/handleError.ts` around lines 223 - 264, Extract the AggregateError
siblings handling from formatCauseChain into a small helper (e.g.,
formatAggregateSiblings(siblings: unknown[], depth: number): string) and replace
the Array.isArray(siblings) block with a call to that helper; the helper should
take the shown = siblings.slice(0,5), loop over shown appending
"\n${indent}[error ${i+1}/${siblings.length}]" and recursively call
formatCauseChain(shown[i], depth+1) for each, add the overflow line
"\n${indent}... ${siblings.length - shown.length} more" when applicable, and
preserve the existing indent computation and return string composition so
formatCauseChain's behavior, recursion into nested cause, and output formatting
remain unchanged.
src/libs/blockchain/gcr/gcr_routines/feeDistribution.ts (1)

146-197: ⚡ Quick win

Consider consolidating parameters into a structured object.

emitTwoRecipientSplit accepts 9 parameters, exceeding typical maintainability guidelines (max 7). While the function works correctly, an object parameter would improve readability and make future changes easier.

♻️ Refactor to use a parameter object
+interface TwoRecipientSplitInput {
+    componentName: "network_fee" | "additional_fee"
+    total: number
+    burnPct: number
+    treasuryPct: number
+    burnAddress: string
+    treasuryAddress: string
+    senderAddress: string
+    txHash: string
+    isRollback: boolean
+}
+
 function emitTwoRecipientSplit(
-    componentName: "network_fee" | "additional_fee",
-    total: number,
-    burnPct: number,
-    treasuryPct: number,
-    burnAddress: string,
-    treasuryAddress: string,
-    senderAddress: string,
-    txHash: string,
-    isRollback: boolean,
+    input: TwoRecipientSplitInput,
 ): GCREditBalance[] {
+    const { componentName, total, burnPct, treasuryPct, burnAddress, treasuryAddress, senderAddress, txHash, isRollback } = input
     if (total <= 0) return []
     // ... rest unchanged

Then update call sites to pass an object.

🤖 Prompt for 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.

In `@src/libs/blockchain/gcr/gcr_routines/feeDistribution.ts` around lines 146 -
197, The function emitTwoRecipientSplit has too many positional params—refactor
it to accept a single parameter object (e.g., EmitTwoRecipientSplitArgs)
containing componentName, total, burnPct, treasuryPct, burnAddress,
treasuryAddress, senderAddress, txHash, and isRollback; update the function
signature and internal references to use the object properties and leave logic
unchanged (references: emitTwoRecipientSplit, makeBalanceEdit, GCREditBalance),
then update all call sites to pass the new object shape instead of nine
positional arguments.
src/utilities/sharedState.ts (1)

266-266: 💤 Low value

Remove inferrable type annotation.

The type number is trivially inferred from the literal 0, making the explicit annotation redundant.

♻️ Optional cleanup
-    additionalFee: number = 0
+    additionalFee = 0

As per coding guidelines, this project uses TypeScript as the primary language, and removing redundant type annotations follows TypeScript best practices.

🤖 Prompt for 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.

In `@src/utilities/sharedState.ts` at line 266, The property declaration
additionalFee: number = 0 has a redundant explicit type since TypeScript can
infer number from the literal; update the declaration for the symbol
additionalFee to remove the explicit ": number" annotation so it reads with the
initializer only (additionalFee = 0) while preserving semantics and any
surrounding comments or visibility modifiers.
🤖 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 `@forking/REHEARSAL_RESULTS.md`:
- Around line 695-706: Add a language identifier to the fenced code block that
contains the PASS lines (the block starting with "PASS 
04-genesis-hash-invariance ...") so markdownlint MD040 is satisfied; update the
opening fence from ``` to ```text (or another appropriate language tag) in the
REHEARSAL_RESULTS.md fenced code block so the block is explicitly marked as
plain text.

In `@forking/RUNBOOK_FORK_ACTIVATION.md`:
- Around line 11-19: Several fenced code blocks in the
RUNBOOK_FORK_ACTIVATION.md diff are missing language identifiers (tripping
MD040); update each unlabeled triple-backtick block to include a language tag
(e.g., "text"). Specifically, add the tag to the ASCII box block starting with
"┌─────────────────", the log examples like "[FORKS] Loaded fork
\"osDenomination\"...", "[FORKS] DEMOS_DISABLE_FORK_MACHINERY...", the fork
table snippets containing "fork_name = osDenomination" and "fork_name =
gasFeeSeparation", the pubkey/balance listing, and the warning line "[WARNING]
[forks][osDenomination] CAP applied..." (and the other occurrences mentioned:
225-228, 234-236, 317-325, 329-334, 338-342, 346-350, 354-356) by changing ```
to ```text so markdownlint no longer flags MD040.

In `@src/errors/handleError.ts`:
- Around line 229-235: The cause output duplicates the error header because you
take the full stack (which already starts with "Name: message") and then prepend
`${cause.name}: ${cause.message}` again; fix this in the if (cause instanceof
Error) block in handleError.ts by removing the stack's header line before
slicing — i.e. split the stack and use .slice(1, 6) (or otherwise drop the first
element) when building `frames`, then keep the explicit ` | cause:
${cause.name}: ${cause.message}\n${frames}` so the name/message appears only
once; update the `frames` construction that currently does
`.split("\n").slice(0, 6)` accordingly.

---

Nitpick comments:
In `@src/errors/handleError.ts`:
- Around line 223-264: Extract the AggregateError siblings handling from
formatCauseChain into a small helper (e.g., formatAggregateSiblings(siblings:
unknown[], depth: number): string) and replace the Array.isArray(siblings) block
with a call to that helper; the helper should take the shown =
siblings.slice(0,5), loop over shown appending "\n${indent}[error
${i+1}/${siblings.length}]" and recursively call formatCauseChain(shown[i],
depth+1) for each, add the overflow line "\n${indent}... ${siblings.length -
shown.length} more" when applicable, and preserve the existing indent
computation and return string composition so formatCauseChain's behavior,
recursion into nested cause, and output formatting remain unchanged.

In `@src/libs/blockchain/gcr/gcr_routines/feeDistribution.ts`:
- Around line 146-197: The function emitTwoRecipientSplit has too many
positional params—refactor it to accept a single parameter object (e.g.,
EmitTwoRecipientSplitArgs) containing componentName, total, burnPct,
treasuryPct, burnAddress, treasuryAddress, senderAddress, txHash, and
isRollback; update the function signature and internal references to use the
object properties and leave logic unchanged (references: emitTwoRecipientSplit,
makeBalanceEdit, GCREditBalance), then update all call sites to pass the new
object shape instead of nine positional arguments.

In `@src/utilities/sharedState.ts`:
- Line 266: The property declaration additionalFee: number = 0 has a redundant
explicit type since TypeScript can infer number from the literal; update the
declaration for the symbol additionalFee to remove the explicit ": number"
annotation so it reads with the initializer only (additionalFee = 0) while
preserving semantics and any surrounding comments or visibility modifiers.
🪄 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: aefc0127-263c-412a-82b2-69e542fa58f4

📥 Commits

Reviewing files that changed from the base of the PR and between 94d379d and f4444ee.

📒 Files selected for processing (37)
  • .bank
  • decimal_planning/DEVNET_READINESS.md
  • decimal_planning/IDEA.md
  • decimal_planning/LOG.md
  • decimal_planning/NEXT_STEPS.md
  • decimal_planning/PAUSED.md
  • decimal_planning/REHEARSAL_PLAN.md
  • decimal_planning/RUNBOOK_FORK_ACTIVATION.md
  • decimal_planning/SPEC_P4.md
  • decimal_planning/audit_node.md
  • decimal_planning/audit_node_post_staking.md
  • decimal_planning/audit_sdk.md
  • decimal_planning/forking_feasibility.md
  • decimal_planning/surface_scan.md
  • forking/REHEARSAL_RESULTS.md
  • forking/RUNBOOK_FORK_ACTIVATION.md
  • forking/decimal_planning/SPEC.md
  • forking/gas_separation/PLAN.md
  • src/config/defaults.ts
  • src/config/types.ts
  • src/errors/handleError.ts
  • src/forks/amountCanonical.ts
  • src/forks/burnAddress.ts
  • src/forks/forkConfig.ts
  • src/forks/loadForkConfig.ts
  • src/forks/migrations/gasFeeSeparation.ts
  • src/libs/blockchain/gcr/gcr_routines/feeDistribution.ts
  • src/libs/blockchain/routines/applyGasFeeSeparation.ts
  • src/libs/blockchain/routines/calculateCurrentGas.ts
  • src/libs/blockchain/routines/loadNetworkParameters.ts
  • src/utilities/sharedState.ts
  • testing/forks/forkGates.test.ts
  • testing/forks/preflight.ts
  • testing/forks/rehearsal/README.md
  • tests/blockchain/applyGasFeeSeparation.test.ts
  • tests/blockchain/feeDistribution.test.ts
  • tests/governance/calculateCurrentGas.test.ts
💤 Files with no reviewable changes (13)
  • decimal_planning/surface_scan.md
  • decimal_planning/LOG.md
  • decimal_planning/audit_node.md
  • decimal_planning/PAUSED.md
  • decimal_planning/SPEC_P4.md
  • decimal_planning/forking_feasibility.md
  • decimal_planning/REHEARSAL_PLAN.md
  • decimal_planning/IDEA.md
  • decimal_planning/audit_node_post_staking.md
  • decimal_planning/DEVNET_READINESS.md
  • decimal_planning/audit_sdk.md
  • decimal_planning/NEXT_STEPS.md
  • decimal_planning/RUNBOOK_FORK_ACTIVATION.md
✅ Files skipped from review due to trivial changes (6)
  • .bank
  • testing/forks/preflight.ts
  • src/config/defaults.ts
  • testing/forks/rehearsal/README.md
  • src/forks/amountCanonical.ts
  • src/config/types.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/governance/calculateCurrentGas.test.ts
  • testing/forks/forkGates.test.ts
  • src/libs/blockchain/routines/loadNetworkParameters.ts
  • src/forks/forkConfig.ts

Comment thread forking/REHEARSAL_RESULTS.md Outdated
Comment thread forking/RUNBOOK_FORK_ACTIVATION.md Outdated
Comment thread src/errors/handleError.ts Outdated
…e tags

Three review-comment fixes bundled. Each maps to a Mycelium task under
epic E#11 (PR #817 review fixes).

myc#102 — [G-1 + G-2] handleNativeOperations.ts (P1, critical)
  Mirror the applyGasFeeSeparation silent-fee-bypass guard onto the two
  post-fork TLSN branches (tlsn_request, tlsn_store). When
  generateSpecialOpsFeeEdits returns [] but the demanded fee is > 0
  (requireFeeDistribution() null because feeDistribution was never
  primed OR every percentage is 0), the previous code spread the empty
  array onto `edits` and the TLSN operation proceeded without removing
  any tokens from the sender. Now both branches throw with a clear
  message so the tx is rejected at validation time. Adds one test per
  branch asserting the throw.

myc#104 — [CR-3] handleError.ts (CodeRabbit minor)
  formatCauseChain was printing `${cause.name}: ${cause.message}`
  explicitly AND keeping that same line from Error.stack[0], producing
  duplicate header lines in the boot log. Skip the stack's first line
  when it begins with `${name}:`. Extracted the AggregateError sibling
  walk into formatAggregateSiblings to drop SonarCloud cognitive
  complexity from 16 to below the 15 threshold flagged in the same
  review.

myc#103 — [CR-1 + CR-2] forking/*.md (CodeRabbit minor)
  Tagged every bare opening fence in REHEARSAL_RESULTS.md and
  RUNBOOK_FORK_ACTIVATION.md with `text` so markdownlint MD040 is
  satisfied. ASCII boxes, log snippets, and table dumps all read as
  plain text; nothing in either file is a real syntax-highlighted
  language.

Tests: 287 pass / 1 pre-existing fail unrelated (governance snapshot
test fails on baseline too — module-level Datasource.getInstance is
undefined; tracked separately).
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/errors/handleError.ts (1)

289-293: 💤 Low value

Consider improving fallback for non-serializable causes.

The String(cause) fallback (line 292) will produce "[object Object]" for plain objects when JSON.stringify fails, reducing diagnostic value. While this is acceptable as a last-resort safeguard, you could check for a custom toString method first:

try {
    return ` | cause: ${JSON.stringify(cause)}`
} catch {
    if (typeof cause === "object" && cause !== null && "toString" in cause) {
        try {
            return ` | cause: ${cause.toString()}`
        } catch {}
    }
    return ` | cause: ${String(cause)}`
}

However, given this is already a fallback path for edge cases (circular references or exotic thrown values), the current implementation is reasonable.

🤖 Prompt for 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.

In `@src/errors/handleError.ts` around lines 289 - 293, In
src/errors/handleError.ts, improve the fallback when JSON.stringify(cause)
throws by, inside the catch block for the existing try that returns ` | cause:
${JSON.stringify(cause)}`, checking whether cause is a non-null object with a
custom toString (i.e., "toString" in cause) and, if so, call that in a nested
try and return ` | cause: ${cause.toString()}`; if that fails or no custom
toString exists, fall back to returning ` | cause: ${String(cause)}`—this
preserves the original behavior but gives more useful diagnostics for plain
objects before resorting to String(cause).
🤖 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.

Nitpick comments:
In `@src/errors/handleError.ts`:
- Around line 289-293: In src/errors/handleError.ts, improve the fallback when
JSON.stringify(cause) throws by, inside the catch block for the existing try
that returns ` | cause: ${JSON.stringify(cause)}`, checking whether cause is a
non-null object with a custom toString (i.e., "toString" in cause) and, if so,
call that in a nested try and return ` | cause: ${cause.toString()}`; if that
fails or no custom toString exists, fall back to returning ` | cause:
${String(cause)}`—this preserves the original behavior but gives more useful
diagnostics for plain objects before resorting to String(cause).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0cf8f109-0777-4ba1-83cf-6ace7c5b5de9

📥 Commits

Reviewing files that changed from the base of the PR and between f4444ee and 0381b81.

📒 Files selected for processing (5)
  • forking/REHEARSAL_RESULTS.md
  • forking/RUNBOOK_FORK_ACTIVATION.md
  • src/errors/handleError.ts
  • src/libs/blockchain/gcr/gcr_routines/handleNativeOperations.ts
  • tests/blockchain/handleNativeOperations.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/libs/blockchain/gcr/gcr_routines/handleNativeOperations.ts
  • tests/blockchain/handleNativeOperations.test.ts

…lizable causes

CodeRabbit nit on PR #817: when JSON.stringify(cause) fails (cycles,
exotic thrown values), the catch block dropped to `String(cause)` which
collapses plain objects to "[object Object]". Try the cause's own
toString first — most thrown objects either carry a useful one or fall
through to String() anyway. Pure diagnostic quality improvement; no
behavior change on the happy path.
@tcsenpai
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@tcsenpai tcsenpai merged commit ffa1f0c into stabilisation May 13, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants