Skip to content

feat: FileRegistry and Streaming File Upload Architecture#407

Open
AndriiS-DevBrother wants to merge 44 commits into
devfrom
feature/streaming-file-upload-registry
Open

feat: FileRegistry and Streaming File Upload Architecture#407
AndriiS-DevBrother wants to merge 44 commits into
devfrom
feature/streaming-file-upload-registry

Conversation

@AndriiS-DevBrother

@AndriiS-DevBrother AndriiS-DevBrother commented Feb 24, 2026

Copy link
Copy Markdown
Collaborator

Streaming File Upload & Distributed Data Availability

This PR implements the complete Streaming File Upload feature for F1R3FLY — enabling native support for large binary file handling (10GB+) directly on the blockchain, without inflating the LMDB state database.

Dependency

  • Python SDK: F1R3FLY-io/pyf1r3fly#4 — client-side uploadFile and downloadFile gRPC streaming implementation

Integration Tests

What Was Added

1. gRPC Streaming Upload (uploadFile)

Client-streaming gRPC endpoint that accepts multi-gigabyte files in 1–4 MB chunks. Files are written directly to disk via FileChannel with direct ByteBuffer allocation (O(1) memory, zero JVM heap pressure). A Blake2b-256 hash is computed incrementally during ingestion and verified at EOF with an atomic .tmp → <hash> rename. Interrupted uploads leave no trace — the .tmp file is immediately cleaned up.

2. Automated Synthetic Deploy

After file ingestion, the node automatically constructs and submits a blockchain transaction (file!("register", ...) on the rho:io:file system channel) linking the file to the chain. The client makes ONE call — if uploadFile succeeds, the file is on disk AND the deploy is in the mempool. The response includes a deployId for lifecycle tracking via existing findDeploy/isFinalized RPCs.

3. FileRegistry.rho — On-Chain Ownership & Deduplication

A Rholang smart contract (modeled after SystemVault.rho) that maintains a per-file registry map (fileHash → { deployers: {pk1: true, ...}, name: "file.ext" }). Provides:

  • Zero-cost deduplication — duplicate uploads skip physical storage but add the new deployer to the reference-counted ownership map
  • AuthKey-gated deletion — only file owners can delete; physical file removal happens only when the last reference is removed
  • Self-initializing genesis — no genesis ceremony dependency

4. P2P Validator File Sync (Layer 2)

Pull-based file replication over the existing TransportLayer. When a validator receives a block referencing files it does not have, it fetches them from the block proposer in 16 MB streaming chunks with Blake2b integrity verification. Hash mismatch → block rejected.

5. gRPC Streaming Download (downloadFile)

Server-streaming gRPC endpoint (observer-only in production, validator-allowed in devMode). Downloads are gated by finalization — the handler queries the FileRegistry at the Last Finalized Block post-state via exploratoryDeploy before serving bytes. Includes resume support via offset field.

6. DA-Optimistic Consensus (Layer 4)

Casper block validation is gated by data availability — validators do not build on blocks until all referenced files are downloaded and verified. Finalization naturally implies DA across participating validators. Includes configurable backpressure: max-file-data-size-per-block (50 GB) and max-file-deploys-per-block (10).

7. Storage-Proportional Phlo Pricing (Layer 5)

File uploads are charged BASE_REGISTER_PHLO + fileSize × phloPerStorageByte with enforcement at both upload-time (early rejection) and block-execution time (via CostAccounting.charge()). Downloads are free.

8. Orphan File Cleanup

When a file-registration deploy expires or is evicted from the mempool, the associated physical file is automatically deleted. The deployLifespan was increased from 50 to 250 blocks to prevent premature cleanup during large P2P replication.

How It Works (High Level)

Client                          Node                         Validators
  │                               │                               │
  ├── uploadFile(stream) ───────► │ Write to disk (O(1) mem)      │
  │   metadata + chunks           │ Blake2b hash verification     │
  │                               │ Atomic .tmp → <hash> rename   │
  │                               │ Auto-create synthetic deploy  │
  │  ◄── FileUploadResponse ────--┤ Push deploy to mempool        │
  │   (fileHash, deployId)        │                               │
  │                               │── Propose block ─────────────►│
  │                               │                               │ Fetch missing files (P2P)
  │                               │                               │ Verify Blake2b hash
  │                               │                               │ Execute & validate block
  │                               │                               │ Build on block (DA-gated)
  │── findDeploy(deployId) ─────► │                               │
  │── isFinalized(blockHash) ───► │                               │
  │                               │                               │
  │── downloadFile(hash) ────────►│ (observer-only)               │
  │  ◄── stream file chunks ─────-┤ Finalization-gated            │

Architecture Documentation

For the full five-layer architecture specification, see streaming_file_upload.md

Comment thread casper/src/main/resources/FileRegistry.rho

@spreston8 spreston8 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Initial Code Review — PR #407

This is a first-pass review covering the Scala infrastructure, P2P protocol, gRPC layer, and integration points. A deeper review of the Rholang contracts (FileRegistry.rho, system process interactions, and consensus replay correctness) will follow separately.

Overall the architecture is well-designed — the five-layer separation, content-addressed storage, and the SystemVault.rho-style AuthKey pattern are solid foundations. The issues below are things that should be addressed before merge.


1. Path traversal in FileRequester — remote peer can read/write arbitrary files

FileRequester.scala:321dataDir.resolve(msg.fileHash) uses the peer-supplied fileHash with no validation. A malicious peer can send fileHash = "../../.rnode/node.key.pem" to exfiltrate the node's private key via handleFileRequest, or write to arbitrary paths via handleFilePacket (lines 43, 56, 63, 191).

FileDownloadAPI already has both regex validation (^[a-f0-9]{64}$, line 29) and normalize().startsWith() defense-in-depth (line 97). FileRequester needs the same treatment on all entry points.

Similarly, SystemProcesses.scala:870 (fileDelete) does dir.resolve(fileHash) with no validation on the Rholang-supplied hash.

2. _deleteTemplate sends to wrong channel — physical file deletion is broken

FileRegistry.rho:252fileProc(`rho:io:file`) resolves to FILE_IO (arity 5, wired to fileRegister). The fileDelete handler lives on rho:io:file:delete / FILE_IO_DELETE (arity 4), as wired in RhoRuntime.scala:544-551. The delete message will never match the register handler's pattern and will block forever.

Tests pass because: (a) the Rholang delete test deliberately uses two deployers so remainingDeployers > 0 and never enters the physical-delete branch, and (b) FileSystemProcessSpec sends directly to FILE_IO_DELETE, bypassing FileRegistry.rho entirely.

Fix: change line 252 to fileProc(`rho:io:file:delete`) and add a test that exercises last-deployer deletion through the contract.

3. FILE_REGISTER missing from nonDeterministicCalls — replay divergence

SystemProcesses.scala:210-218FILE_DELETE (31L) is correctly listed as non-deterministic with a replay branch (line 847-853). FILE_REGISTER (30L) is not listed, and the fileRegister handler has no replay branch. However, it produces side-effect data to FILE_REGISTRY_NOTIFY (line 812). During block replay on other validators, the handler re-executes and double-produces to that channel, breaking consensus.

Fix: add FILE_REGISTER to nonDeterministicCalls and add a case isContractCall(produce, true, previousOutput, ...) replay branch.

4. Signature verification bypassed — any deploy can register phantom files

SystemProcesses.scala:781val verified = true is hardcoded. The rho:io:file channel is exposed as a write-only bundle via the URN map, so any user deploy can call file!("register", "nonexistent_hash", 1, "fake.txt", *ack). The handler constructs a real GSysAuthToken (line 800-802) and produces to FILE_REGISTRY_NOTIFY, which FileRegistry.rho consumes and creates a real on-chain entry. The attacker pays phlo but pollutes the registry with hashes that don't exist on any node's disk, triggering phantom P2P fetch attempts across the network.

5. MissingFileData blocks are permanently discarded instead of held for retry

MultiParentCasperImpl.scala:665-670MissingFileData falls through to the generic InvalidBlock handler which calls CasperBufferStorage.remove(). The comment at BlockStatus.scala:94-97 correctly notes this shouldn't be slashable because files may still be in transit, but the block is still deleted rather than held. If P2P transfer completes later (which can take hours per the design doc), the block is already gone.

Fix: MissingFileData should return the block to a pending state for retry, not discard it.

6. No retry on P2P download hash mismatch

FileRequester.scala:309-316 — when finalizeDownload detects a hash mismatch, it deletes the temp file and removes download state but does not re-request from another peer. Combined with #5, a single corrupted transfer can permanently prevent a node from validating a block.

Fix: on mismatch, reset download state and re-request from a different peer. Consider negative peer scoring (the TODO at line 310-311).

7. Peer-controlled OOM in handleFileRequest

FileRequester.scala:333ByteBuffer.allocate(msg.chunkSize) uses the peer-supplied chunkSize directly. A malicious peer can send chunkSize = Int.MaxValue to trigger a 2GB allocation.

Fix: clamp to local configured max, e.g. math.min(msg.chunkSize, this.chunkSize).

8. FileChannel resource leak in handleFileRequest

FileRequester.scala:331-337FileChannel.open() and channel.close() are not in try-finally. An IOException during channel.read(buf) leaks a file descriptor.

9. TOCTOU race in handleHasFile

FileRequester.scala:168-184 — three separate Ref.get calls plus a filesystem check, then conditionally calls startDownload. startDownload (line 193) does downloads.update(_ + ...) which unconditionally overwrites, potentially resetting an in-progress download. requestFiles (line 60-68) correctly uses atomic downloads.modifyhandleHasFile should use the same pattern.

10. DA-gate broadcasts FileRequest to ALL connected peers

FileReplicationSetup.scala:57peers.toList.traverse_ sends to every peer. All peers with the file respond with full chunk streams, but only the fastest peer's chunks are accepted (offset matching). For a 6GB file with 10 peers, ~90% of P2P bandwidth is wasted.

Consider sending only to the block proposer (who is guaranteed to have the file) plus 1-2 fallback peers.

11. _sysAuthTokenCh unbounded tuplespace accumulation

FileRegistry.rho:150 — every registerNotify call unconditionally produces to _sysAuthTokenCh. Rholang channels are multiset queues, not registers. After N file registrations, N copies of the token accumulate. _deleteTemplate reads with <<- (peek) so correctness is preserved, but tuplespace storage grows linearly with file count.

Fix: produce only once using a flag channel, or use consume-then-reproduce pattern.

12. registerNotify silently discards errors

FileRegistry.rho:148-152regAckCh is created but never consumed. If FileRegistry!("register", ...) fails, the system process has already returned (true, fileHash) to the deploy caller, creating a disconnect between system process result and on-chain state.

13. System.out.println debug statements bypass logging framework

CasperLaunch.scala:111,185,224"sendBufferPendantsToCasper", "connectAsGenesisValidator", "initBootstrap" print directly to stdout, bypassing SLF4J. These should use Log[F].info(...) or be removed.

14. extractFileSize returns 0 on parse failure, bypassing block backpressure

OrphanFileCleanup.scala:57-58 — if the size field in a file-registration deploy is malformed, extractFileSize returns 0. This causes the deploy to bypass the per-block maxFileDataSizePerBlock limit in BlockCreator.

Fix: return Option[Long] and exclude unrecognizable deploys from file-deploy selection.

15. completed set in FileRequester never pruned

FileRequester.scala:36Set[String] grows monotonically with every downloaded file hash. Should be pruned after confirmation on disk or use a bounded structure.

16. FileAvailability.findMissingFiles does blocking I/O outside F

FileAvailability.scala:69-71 — pure function calls Files.exists() synchronously, returning List[String] not F[List[String]]. Blocks the calling fiber's thread.

17. Metadata sidecar race on concurrent dedup uploads

FileUploadAPI.scala:327-328 — two concurrent uploads of the same content both write .meta.json via Files.write(). Last writer wins with potentially incorrect fileName.

18. deployLifespan = 250 hardcoded

MultiParentCasperImpl.scala:684 — compile-time constant coupled to network bandwidth assumptions. Should be configurable via CasperShardConf.

19. Minor cleanup

  • SystemProcesses.scala:768blockInfo <- blockData.get result never used. Dead code from removed sig verification.
  • FileSystemProcessSpec:44-46 — empty test body for "register with malformed hex signature". Remove or mark pending.

Co-Authored-By: Claude noreply@anthropic.com

@metaweta

Copy link
Copy Markdown
Collaborator

This is great, thanks! How many of these attacks apply to the code this PR was based on?

@NazarY-DevBrother NazarY-DevBrother left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Additional Review Findings (not covered by @spreston8's review)

Security / Bugs

  1. DeployGrpcServiceV1.scala:uploadFile — On signature validation failure, cleanup deletes uploadDir.resolve(hash). If this was a deduplicated upload (file already existed before this request), the pre-existing file is deleted, breaking other clients who successfully uploaded the same content earlier.

  2. FileDownloadAPI.scala_maxIpEntries is a mutable var on a singleton object, overwritten on every streamFile call without synchronization. Two concurrent requests with different maxCacheEntries race on this write. Should be set once at initialization.

  3. OrphanFileCleanup.scala:isFileRegistrationDeploy — Since this string-matching detection also drives DA-gating in MultiParentCasperImpl, a crafted regular deploy containing "rho:io:file", "register", and a 64-char hex string in a comment could force validators to stall for fileSyncTimeout (2h) waiting for a non-existent file.

  4. FileRequester.scala:awaitFiles — Timeout cleanup races with finalizeDownload. Between deciding a hash is "still missing" and running downloads.modify, a concurrent fiber could complete the download. This can leave inconsistent state where the file exists on disk but is in neither downloads nor completed.

  5. FileRequester.scala — Mutable Blake2bDigest object is shared across atomic downloads.modify boundary via shallow .copy(). If a retry or concurrent operation touches the old DownloadState reference after modify, the digest corrupts silently.

Edge Cases

  1. FileUploadAPI.scala:validateMetadatamaxFileSize is checked only against the client-declared metadata.fileSize, not actual bytes received. A client can declare a valid size but stream more data.

  2. FileUploadAPI.scala:finalizeUpload — Atomic rename (.tmp → final) and .meta.json write are two separate operations. JVM crash between them leaves a file without metadata sidecar, with no recovery path.

  3. Costs.scala:fileStorageCostMath.multiplyExact throws ArithmeticException on overflow but callers don't catch it. At high phloPerStorageByte or large files, this propagates as an unhandled exception through the evaluator.

  4. FileReplicationSetup.scala:create — Falls back to relative path Paths.get("file-replication") when fileReplicationDir is None. Location depends on JVM working directory — unpredictable in production.

  5. DeployGrpcServiceV1.scala:checkFileFinalized — FileRegistry contract address hardcoded as a string literal in exploratoryDeploy. If the address changes (upgrade, different shard), all downloads silently fail.

  6. FileDownloadAPI.scala:doStreamFiles.size(filePath) called twice (once for logging, once for metadata chunk). File could be deleted between calls by orphan cleanup. Should capture size once.

  7. FileDownloadAPI.scala — No validation that offset < fileSize. An offset beyond EOF silently returns an empty data stream with a metadata chunk claiming the full file size.

  8. DeployServiceV1.proto:FileUploadMetadata.term — Client provides raw Rholang term. If the signed term isn't the canonical rho:io:file registration, deploy enters mempool but never registers the file, creating an orphan with no cleanup trigger.

DRY

  1. Hex regex "^[a-f0-9]{64}$" hardcoded in 3 places. toHex conversion duplicated in 3 places. Both should be shared.

  2. File cleanup logic (deleteIfExists for data + .meta.json) copy-pasted in two error branches within DeployGrpcServiceV1.uploadFile.

  3. FileReplicationSetup.create + destructure pattern repeated identically in 3 call sites (CasperLaunch, GenesisCeremonyMaster, Initializing).

Tests

  1. FileDeploySelectionSpec:applyBackpressure — Uses takeWhile (stops at first over-budget), but production BlockCreator uses foldLeft (skips and continues). Divergent behavior not covered by tests.

Co-Authored-By: Claude noreply@anthropic.com

- Validate phlo limit before any file I/O; enforce strict byte count
  during streaming to prevent disk exhaustion
- Enforce byte limit during P2P file sync to block infinite garbage
  streams from malicious proposers
- Add regex validation on downloadFile fileHash to prevent path traversal
- Cap P2P file sync concurrency to avoid I/O contention with client downloads
- Update FileRegistry.rho comments to reflect enforced ingestion guarantees
- Add expectedFileHash field to FileUploadMetadata (field 11)
- Add FileUploadResult message (fileHash, deployId, cost fields)
- Update FileUploadResponse to wrap FileUploadResult
- Add FileDownloadRequest, FileDownloadMetadata, FileDownloadChunk messages
- Add downloadFile RPC to DeployService (observer-only)
- Register HashM instances for all 7 new proto message types
- Update FileUploadAPI.scala return type to FileUploadResult
- Add FileUploadProtoSpec: 9 round-trip serialization tests (all pass)

sbt models/compile   -> success
sbt casper/compile   -> success
sbt models/testOnly coop.rchain.models.FileUploadProtoSpec -> 9/9 passed
- Add FileUploadAPI with streaming Blake2b-256 hashing (BouncyCastle),
  FileChannel writes, dedup fast-path, overflow guard, atomic rename,
  hash collision handling, and interruption cleanup via Task.guarantee
- Add FileMetadata with circe-based JSON serde
- Wire uploadFile RPC in DeployGrpcServiceV1; add downloadFile stub
- Thread uploadDir (dataDir/file-replication) through APIServers and
  Setup.scala, created eagerly with Sync[F].delay at startup
- All IO suspended in Task.delay / Sync[F].delay throughout
- Add FileUploadAPISpec (10 tests, all passing)
- Replace broken StreamStatusSpec with pending stub (stale proto types)
- Mark Task 2 as Done in progress.md
… progress

- Add uploadDir: Path param to DeployGrpcServiceV1.apply and APIServers.build
- Create uploadDir eagerly via Sync[F].delay in Setup.scala at node startup
- Mark Task 2 as Done in progress.md
- Add OrphanFileCleanup utility with term detection, hash extraction,
  cross-reference check, and physical file + meta.json deletion
- Wire cleanup into BlockCreator.prepareUserDeploys (expiration path only)
  — reuses pre-computed valid/expired sets, zero extra readAll calls
- Add fileReplicationDir field to CasperShardConf (Option[Path] = None)
  threaded via CasperLaunch.of -> Setup.scala
- Detection requires rho:io:file + "register" + 64-char hex hash in term
  to prevent false positives
- 10 unit tests in OrphanFileCleanupSpec, all passing
- Regression: batch1.*, batch2.*, BlockCreatorSpec (48 tests) all green
- Update task-04 doc and mark progress.md Task 4 done
- Add FileUploadConf case class (chunkSize, replicationDir,
  phloPerStorageByte, baseRegisterPhlo, maxConcurrentDownloadsPerIp)
- Wire FileUploadConf into NodeConf via pureconfig
- Add file-upload section to defaults.conf with all defaults
- Add 4 CLI flags to Options.scala run subcommand:
  --file-upload-chunk-size, --file-replication-dir,
  --file-upload-phlo-per-storage-byte, --max-concurrent-downloads-per-ip
- Wire CLI flags through ConfigMapper to HOCON keys
- Replace hardcoded 'file-replication' strings in Setup.scala
  with conf.fileUpload.replicationDir (config-driven)
- Add FileUploadConfigSpec (8 tests: defaults, CLI overrides, dir ops)
- Update HoconConfigurationSpec and ConfigMapperSpec with FileUploadConf
- Fix 2 pre-existing test mismatches (cleanupInterval, disableLateBlockFiltering)
- Add FILE_IO channel (byteName(32)), FILE_REGISTER (30L) / FILE_DELETE (31L) body refs
- Implement fileRegister: Ed25519 envelope verification over hash:size, wrapped in Try
- Implement fileDelete: SysAuthToken verification + physical file removal from fileReplicationDir
- Mark FILE_DELETE as nonDeterministicCalls with replay branch (skips re-deletion on replay)
- Two Definitions: register (arity 6) and delete (arity 4) sharing FILE_IO channel
- Thread fileReplicationDir: Option[Path] through full RhoRuntime creation chain
- Add mkRuntimeWithFileDir helper in Resources.scala for test use
- FileSystemProcessSpec: 7 tests including Files.exists assertion for delete
- Regression: RuntimeSpec 6/6 pass

Deferred to Tasks 7/10: phlo charging, FileRegistry delegation, genesis sysAuthToken wiring
…tests

- FileRegistry.rho: add rs!() insertSigned URI registration
- FileRegistry.rho: fix 5 pre-existing Rholang bugs surfaced by genesis
  compilation (bundle+ pattern match, filter lambda, missing | separator,
  name/process context mismatch, List->Map for deployers)
- StandardDeploys.scala: add fileRegistry deploy entry and public key
- Genesis.scala: include fileRegistry in genesis deploy sequence
- FileRegistryTest.rho: 7 Rholang unit tests via RhoSpec (all passing)
- FileRegistrySpec.scala: Scala test runner
- docs: update progress.md (Task 7 Done), task-07 notes, streaming_file_upload.md
  to reflect deployers Map structure and Rholang runtime limitations
- Add FileRequester: request/serve/receive files over P2P (Blake2b-256 hash, size guard, atomic rename)
- Add FileAvailability: block deploy scanning + local existence check
- Wire fileAvailability check into Validate.scala and MultiParentCasperImpl
- CasperShardConf: add fileReplicationDir, fileChunkSize, fileSyncTimeout
- Config chain: FileUploadConf -> Setup -> CasperLaunch -> CasperShardConf -> Engine -> FileRequester
- defaults.conf: add file-upload.file-sync-timeout with tuning guidance
- Options.scala / ConfigMapper.scala: add --file-sync-timeout CLI flag
- Tests: FileAvailabilitySpec (10), FileReplicationSpec (4, +overflow test)
- task-05 subtasks: 12/14 complete; PeerScore + integration test deferred
- progress.md: task-05 -> Done
- New FileDownloadAPI with observer gate, hash validation, resume, rate limiting
- Wire downloadFile in DeployGrpcServiceV1 → FileDownloadAPI.streamFile
- Config-driven chunk size and max-concurrent-downloads from defaults.conf
- Thread config through Setup → APIServers → DeployGrpcServiceV1
- FileDownloadAPISpec: 8 test cases covering all spec scenarios (all pass)
- Update progress.md: Task 8 ✅ Done
- Update task-08 doc: subtasks done + implementation notes
- Implement DA-gating validation in MultiParentCasperImpl.
- Add file synchronization with caching/timeout before block validation.
- Implement file-aware deploy selection backpressure in BlockCreator.
- Provide configuration max-file-data-size-per-block and max-file-deploys-per-block.
- Unit testing with DAGateSpec and FileDeploySelectionSpec.
- Update tracking documents.
Implement storage-proportional phlo charging for file uploads, enforced
at two points: upload-time (early rejection) and block execution-time
(consensus enforcement).

Production changes:
- FileUploadCosts.scala: cost formula with overflow protection
- FileUploadAPI.scala: upload-time phlo validation
- SystemProcesses.scala: fix Ed25519→Secp256k1, SHA-256 pre-hash,
  add FILE_IO_DELETE channel (fixes install overwrite bug)
- RhoRuntime.scala: fileDelete uses FILE_IO_DELETE
- Costs.scala: fileStorageCost function
- DeployGrpcServiceV1/APIServers/Setup: wire phloPerStorageByte config

Bug fixes in fileRegister system process:
1. Wrong signature algorithm (Ed25519→Secp256k1)
2. Missing SHA-256 pre-hash (Secp256k1 requires 32-byte input)
3. Channel conflict: fileDelete overwrote fileRegister in HotStore
   (installedContinuations is single-valued per channel)

Tests:
- FileUploadCostSpec: 10 unit tests for cost calculation & upload-time
- StorageCostChargeSpec: 5 runtime-level tests with valid Secp256k1 sig
- StorageCostEnforcementSpec: 6 Casper-level tests with real validator key
- FileSystemProcessSpec: updated for FILE_IO_DELETE channel
- ConfigMapperSpec/HoconConfigurationSpec: configuration tests
- Add FileRegistryInitDeploy system deploy: passes GSysAuthToken to
  FileRegistry!("init", ...) during computeGenesis after blessed terms
- Add FILE_REGISTRY_NOTIFY fixed channel (byte 34) for fileRegister→
  FileRegistry delegation bridge
- Update fileRegister handler to produce (fileHash, fileName, deployerId,
  GSysAuthToken()) to notify channel after sig verification + charging
- Add persistent registerNotify bridge consumer in FileRegistry.rho that
  forwards delegation data to FileRegistry!("register", ...)
- Add rho:io:file:registerNotify URN map entry (Bundle readFlag=true)
- Update FileRegistryTest.rho: remove redundant init call (genesis handles it)
- Update progress.md: Task 6 → Done
- Update task-06: mark deferred items as complete

Tests: FileRegistrySpec 8/8, PoSSpec 15/15, FileSystemProcessSpec 7/7,
RuntimeSpec 6/6
…zing

Remove the separate FileRegistryInitDeploy system deploy that was running
after blessed terms in computeGenesis, causing a state hash divergence
between bootstrap and validator nodes ('Tuplespace hash mismatch').

FileRegistry.rho now stores the SysAuthToken lazily via registerNotify
on first file registration, eliminating the need for a separate init step.

Changes:
- FileRegistry.rho: remove init contract, registerNotify stores token
- RuntimeSyntax.scala: remove FileRegistryInit step from computeGenesis
- Delete FileRegistryInitDeploy.scala (no longer needed)
- Add task-13 documentation

All BlockApproverProtocolTest tests pass (6/6).
AndriiS-DevBrother and others added 25 commits March 26, 2026 13:48
processFileUpload called headOptionL then drop(1) on the same hot
gRPC Observable, creating two subscriptions. The second subscription
saw zero data bytes, causing 'Size mismatch: received 0' errors.

Replace with toListL to materialize the stream once into a List,
then split head (metadata) and tail (data chunks) from the cold
collection. This fixes all 7 file upload integration test failures.
Three bugs fixed in the P2P file replication pipeline:

1. DA callback fire-and-forget: CasperLaunch, GenesisCeremonyMaster,
   and Initializing callbacks spawned background fibers and returned
   List.empty immediately. Block validation proceeded before files
   arrived. Fix: synchronous broadcast + awaitFiles with bounded timeout.

2. Single-peer download routing: FileRequester.requestFiles only sent
   FileRequest to the first peer (bootstrap). If bootstrap didn't have
   the file, download stalled. Fix: broadcast FileRequest to ALL peers.

3. Race condition in handleFilePacket: multiple FilePacket responses at
   the same offset both passed non-atomic offset validation, causing
   duplicate data writes and hash verification failure. Fix: atomic
   Ref.modify (CAS) ensures exactly-once processing per offset.

4. Wire HasFile/FileRequest/FilePacket into CasperPacketHandler via
   package.scala so P2P file messages are correctly deserialized.
Remove temporary debug logging added during P2P replication diagnosis
from FileAvailability, FileDownloadAPI, and DeployGrpcServiceV1.
Proper Log[F] framework logging in FileRequester is retained.
Add test case verifying that file downloads are allowed on non-read-only
nodes when devMode is true. Update test helper to accept devMode param.
Add task-14-p2p-replication-fix.md documenting root cause analysis and
fixes for the three P2P file replication bugs. Update progress.md to
include Task 14 and mark Task 12 (Integration Tests) as Done.
Replace chunks.toListL (which buffered the entire file in JVM heap)
with a single-pass foldLeftL state machine:

  WaitingForMetadata → WritingData → Dedup | Failed | finalize

Memory usage drops from O(file-size) to O(chunk-size ≈ 4MB),
eliminating OOM crashes on large uploads (6GB+).

- Remove processUpload and streamToFileAndHash (inlined into fold)
- Add UploadState sealed trait: WaitingForMetadata, WritingData, Failed, Dedup
- Use AtomicReference to track state for mid-stream cleanup on gRPC cancel
- Harden cleanup handler so filesystem errors never replace original error
- finalizeUpload now handles its own .tmp cleanup on size/hash mismatch

Public API signature (processFileUpload) is unchanged.
Critical fixes:
- FileRequester: TOCTOU race in requestFiles (atomic downloads.modify)
- FileRequester: double toByteArray allocation in handleFilePacket
- FileRequester: blocking Files.exists/Files.size outside Sync[F].delay
- DeployGrpcServiceV1: blocking Files.deleteIfExists on gRPC thread

Medium fixes:
- FileRegistry.rho: unbounded _sysAuthTokenCh growth (one-shot flag)
- FileRegistry.rho: globally-accessible @{["fileState", hash]} state
  channels replaced with unforgeable private fileStateCh per handle
- FileDownloadAPI: path traversal defense-in-depth check
- FileDownloadAPI: mutable var bytesStreamed replaced with scan
- FileUploadAPI: fileSize <= 0 and fileHash format validation
- BlockStatus: document MissingFileData not slashable

Minor fixes:
- FileDownloadAPI: ByteBuffer reuse via clear() instead of per-chunk alloc
- Validate: remove dead fileAvailability method

Test update:
- FileUploadAPISpec: use valid 64-char hex hash in mismatch test
Critical fixes:
- C1: Replace unbounded ConcurrentHashMap with bounded LRU cache in FileDownloadAPI
- C2: Document mutable aliasing invariant in FileRequester.handleFilePacket
- C3: Clean up stale .part files on download timeout in FileRequester.awaitFiles
- C4: Tighten FileHashPattern regex in OrphanFileCleanup

Medium fixes:
- M1: Add max file size validation to FileUploadAPI.validateMetadata
- M2: Replace mutable var with foldLeft in BlockCreator
- M3: Return Either from FileMetadata.fromJson for safer parsing
- M4: Extract FileReplicationSetup helper to DRY engine init code
- M5: Guard against negative fileSize in SystemProcesses.fileRegister
- M6: Document global rate limiter limitation in DeployGrpcServiceV1
- M7: Use idempotent token storage in FileRegistry.rho
- M8: Thread phloPerStorageByte through ProcessContext

Config extraction:
- Extract max-file-size (10 GB) and max-download-cache-entries (10000)
  from hardcoded constants into defaults.conf and FileUploadConf model
- Thread config values through Setup → APIServers → gRPC service → APIs
Consolidated file upload configuration passing across the codebase.
Instead of scattering individual fields across multiple method signatures,
config is now passed as grouped objects:
- casper layer: New FileConf case class groups 5 file-related fields inside CasperShardConf
- node layer: FileUploadConf is passed directly to API servers
- Tests: Fixed missing maxFileSize and maxDownloadCacheEntries in tests
Downloads are now rejected unless the file hash is registered in the
FileRegistry contract at the Last Finalized Block's post-state.  The
check is mandatory on all node types (devMode only opens the API to
validators but does NOT bypass finalization).

Implementation:
- FileDownloadAPI.streamFile: mandatory finalizationChecker callback,
  guard order: observer → hash → path traversal → file exists →
  finalization → rate-limit → stream
- DeployGrpcServiceV1: extracted checkFileFinalized() private method
  with defense-in-depth require() against Rholang injection, error
  logging on exploratoryDeploy failure
- Unfinalized files return NOT_FOUND (no information leakage)

Tests: 15 pass (9 existing + 6 finalization gate tests including
checker-throws semaphore-leak regression)

Docs: streaming_file_upload.md sections 3.4 and 3.7 updated
… during 6GB P2P replication

With default casper-loop-interval=30s, the LFB lags 100+ blocks behind the
tip while a 6GB block is being validated. A file-registration deploy expires
after deployLifespan=50 blocks (~5min), triggering OrphanFileCleanup before
P2P replication completes — file deleted at ~35%, permanently stalling replication.

Setting deployLifespan=250 (~25min) ensures the deploy stays in the mempool
long enough for 6GB P2P replication to complete (~8-15min).

Changed in:
- MultiParentCasperImpl.scala: companion object constant (50→250)
- Setup.scala: positional CasperShardConf arg for GC loop (50→250)
…ed code

- Remove nodeSigHex from fileRegister API (matches commit 4465d78)
- Update P2P chunk size default: 4MB → 16MB
- Consolidate defaults.conf config into single file-upload section
- Fix config key: file-fetch-timeout → file-sync-timeout (2 hours)
- Add max-file-size, max-download-cache-entries config entries
- Note deployLifespan 50→250 in module table
- Add FileDownloadAPI, FileReplicationSetup, OrphanFileCleanup to module inventory
- Fix FileSystemProcess.scala → SystemProcesses.scala (actual location)
- Describe FileUploadAPI as O(1) streaming state machine
- Remove stale task breakdown docs
@AndriiS-DevBrother
AndriiS-DevBrother force-pushed the feature/streaming-file-upload-registry branch from f2cd28d to 70d08db Compare March 27, 2026 21:18
@AndriiS-DevBrother
AndriiS-DevBrother requested review from NazarY-DevBrother and spreston8 and removed request for nashef March 30, 2026 17:36
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.

4 participants