Skip to content

Fix security-review findings on #477 (storage programs) - #1041

Draft
hackobi wants to merge 7 commits into
storagefrom
claude/fix-node-pr477
Draft

hackobi wants to merge 7 commits into
storagefrom
claude/fix-node-pr477

Conversation

@hackobi

@hackobi hackobi commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Fixes for the security-review findings on #477 (Storage Programs). This PR targets the storage branch, so merging it adds these commits to #477.

# Severity Finding Status Commit
1 critical The node rewrote gcr_edits after hashing, so the tx became incoherent and broke mempool merges Fixed: the storage edit is now derived from the signed tx (HandleGCR.getTxEdits), and tx.content is never modified 3bd030d6, 4da60ed9
4 medium The client's gas and nonce edits were dropped (fee/nonce bypass) Fixed by the same change: signed gas and nonce edits are kept, and the storage edit is applied last 3bd030d6
2 high Storage edits ignored isRollback, so a rollback re-applied them Partly fixed: CREATE rollback deletes the row it created; WRITE, UPDATE_ACCESS_CONTROL and DELETE rollback now fail visibly instead of re-applying 21b9c38e
3 high The caller chose the storage address and salt was unused (squatting/front-running) Fixed: CREATE requires deriveStorageAddress(sender, programName, salt) and every op requires the stor- format 2021cc92
5 medium getStorageProgram can never authenticate a reader Not fixed. Reads already fail closed, and tests now pin that behaviour; real fix needs a signed-read protocol in the SDK 6644f9fe
6 low Size, depth and key-length limits were only enforced at RPC Fixed: also enforced at GCR apply 5411d825
7 high No tests 24 tests under tests/storageprogram/ all

4da60ed9 also fixes applyToTx rolling back twice, and rolling back for real during a simulation.

Still open (author decision)

  • Exact undo for WRITE / UPDATE_ACCESS_CONTROL / DELETE needs recorded prior state.
  • Signed reads (finding 5).
  • @kynesyslabs/demosdk is ^2.4.24 with no lockfile; a fresh install resolves 2.12.2, which breaks this code path. Please pin the SDK or commit a lockfile.
  • Please confirm whether this PR is superseded by newer storage-program work on testnet.

Tests: bun test tests/storageprogram: 24 pass.

🤖 Generated with Claude Code

CREATE took storageAddress from the payload as-is and the salt was unused,
so any sender could claim any unused GCR_Main pubkey, including the address
a victim's SDK derives deterministically or a regular account row.

Require every storage operation to target a stor-{40 hex} address, and
require CREATE's address to equal StorageProgram.deriveStorageAddress(sender,
programName, salt) from the SDK.

Adds bun:test unit tests under tests/storageprogram.
validateStorageProgramData (size, nesting depth, key length) ran only in the
RPC-side handler, while applyStorageProgramEdit, which every node runs,
re-checked size only. Run the full validation on CREATE and WRITE edits
when they are applied.
applyStorageProgramEdit ignored editOperation.isRollback, so a consensus
rollback (PoRBFT rollbackGCREditsFromTxs -> applyToTx(tx, true)) re-ran
CREATE, WRITE, UPDATE_ACCESS_CONTROL and DELETE forward.

- CREATE rollback removes the row, but only when that row was created by
  the same tx (createdTxHash is now recorded in the metadata).
- WRITE, UPDATE_ACCESS_CONTROL and DELETE overwrite state that the edit
  does not carry, so their rollback now fails visibly instead of applying
  the edit a second time. Full inversion needs recorded prior state, which
  is a design decision left open.
…gcr_edits

handleExecuteTransaction replaced tx.content.gcr_edits with node-generated
edits after the hash was fixed. sha256(JSON.stringify(content)) then no
longer matched tx.hash, so peers saw the tx as incoherent and
Mempool.receive rejected the whole merged batch. The replacement also
dropped the client's signed gas and nonce edits (SDK 2.4.24 GCRGeneration
emits exactly [balance, nonce] for storageProgram txs), so storage txs paid
no fee and did not bump the nonce.

- The execute path still validates the payload but no longer touches
  tx.content.
- HandleGCR.getTxEdits derives the storage edit from the signed tx
  (content.data, content.from, content.timestamp, hash) and appends it
  after the signed gas and nonce edits. applyToTx and the PoRBFT forward
  apply both use it, so every node applies the same edits. Client-supplied
  storageProgram edits are rejected.
- created/lastModified come from content.timestamp instead of Date.now(),
  so the derived edit is identical on every node.
nodeCall is a no-auth RPC method, so getStorageProgram only ever sees an
anonymous reader. Add tests that pin the current behaviour: public reads
succeed, private reads are refused, and caller-supplied sender or
requesterAddress values grant nothing. Authenticated reads need a client
protocol change and are left open.
applyToTx rolled back the applied edits twice on a failing edit (once
before throwing and again in the catch), and HandleGCR.rollback always
applied for real, even when the forward edits were only simulated.

Storage edits now run after the gas and nonce edits, so a storage edit
that fails on the RPC simulate path (e.g. a WRITE denied by access
control) would have credited the sender's balance and decremented the
nonce for real, twice. Roll back once, and pass simulate through to the
rollback.
@coderabbitai

coderabbitai Bot commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hackobi

hackobi commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Automated launch review: needs-changes at 4da60ed

An automated reviewer checked this PR for the Demos mainnet launch, using public research on how established chains handle the same problem. Each finding cites the code at this head. If a finding is wrong, reply here with the reason.

  1. [high] Consensus applies storage edits without per-tx atomicity; failed storage edits leave gas and nonce applied and diverge from Sync

    applyGCREditsFromMergedMempool applies each edit with no rollback and puts the tx hash in successfulTxs and failedTxs once per edit. The derived storage edit comes last and can fail when applied during a block even though simulation passed: two CREATEs for one address in a block, a DELETE then a WRITE, or a WRITE after an ACL change. When it fails, the gas and nonce edits stay applied. failedTxs are removed only from the DB mempool, and forgeBlock still uses tempMempool. Sync's applyToTx rolls the same tx back and returns false, so syncGCRTables halts. Validators and syncing nodes therefore disagree. The per-edit loop already existed, but this PR is what sends failure-prone storage edits into it.

    Code: src/libs/consensus/v2/PoRBFT.ts:394-404, src/libs/consensus/v2/PoRBFT.ts:136-150, src/libs/consensus/v2/PoRBFT.ts:184-184, src/libs/blockchain/routines/Sync.ts:437-444

  2. [high] Failing WRITE/ACL/DELETE rollback causes gas and nonce edits to be rolled back twice (net re-charge)

    On BlockInvalidError, rollbackGCREditsFromTxs calls applyToTx(tx, true), which applies the edits in forward order with rollback=true. The storage rollback now fails, so the error handler calls rollback(tx, appliedEdits), and apply(edit, tx, true) runs on the gas and nonce edits again. apply sets isRollback on the shared object, and the Balance and Nonce routines flip editOperation.operation in place. The second pass therefore flips the operation back and re-applies the original charge, while the storage change stays. These are the same objects as in tx.content.gcr_edits, because getTxEdits copies only the array. The failure result is ignored by consensusRoutine.

    Code: src/libs/blockchain/gcr/handleGCR.ts:725-736, src/libs/blockchain/gcr/handleGCR.ts:255-258, src/libs/blockchain/gcr/gcr_routines/GCRBalanceRoutines.ts:36-39, src/libs/consensus/v2/PoRBFT.ts:349-363

  3. [low] Rollbacks change the signed edit objects in tx.content.gcr_edits

    getTxEdits returns the signed edit objects themselves, not copies. A rollback sets isRollback and flips operation on them, which changes tx.content after it was hashed, contradicting the PR's claim that 'tx.content is never modified'. On the paths reviewed, the mutated txs are rejected or removed from the mempool, so the effect is limited. The behaviour predates this PR.

    Code: src/libs/blockchain/gcr/handleGCR.ts:635-637

  4. [medium] Finding 5 (storage read auth) remains open

    Reads of private and restricted programs fail closed for every caller, deployer included, because the no-auth nodeCall passes sender "". The data is also stored in plaintext in the tx payload. The PR deliberately leaves this unresolved.

    Code: src/libs/network/manageNodeCall.ts:213-226

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.

1 participant