feat: FileRegistry and Streaming File Upload Architecture#407
feat: FileRegistry and Streaming File Upload Architecture#407AndriiS-DevBrother wants to merge 44 commits into
Conversation
spreston8
left a comment
There was a problem hiding this comment.
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:321 — dataDir.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:252 — fileProc(`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-218 — FILE_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:781 — val 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-670 — MissingFileData 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:333 — ByteBuffer.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-337 — FileChannel.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.modify — handleHasFile should use the same pattern.
10. DA-gate broadcasts FileRequest to ALL connected peers
FileReplicationSetup.scala:57 — peers.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-152 — regAckCh 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:36 — Set[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:768—blockInfo <- blockData.getresult never used. Dead code from removed sig verification.FileSystemProcessSpec:44-46— empty test body for "register with malformed hex signature". Remove or markpending.
Co-Authored-By: Claude noreply@anthropic.com
|
This is great, thanks! How many of these attacks apply to the code this PR was based on? |
NazarY-DevBrother
left a comment
There was a problem hiding this comment.
Additional Review Findings (not covered by @spreston8's review)
Security / Bugs
-
DeployGrpcServiceV1.scala:uploadFile— On signature validation failure, cleanup deletesuploadDir.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. -
FileDownloadAPI.scala—_maxIpEntriesis a mutablevaron a singleton object, overwritten on everystreamFilecall without synchronization. Two concurrent requests with differentmaxCacheEntriesrace on this write. Should be set once at initialization. -
OrphanFileCleanup.scala:isFileRegistrationDeploy— Since this string-matching detection also drives DA-gating inMultiParentCasperImpl, a crafted regular deploy containing"rho:io:file","register", and a 64-char hex string in a comment could force validators to stall forfileSyncTimeout(2h) waiting for a non-existent file. -
FileRequester.scala:awaitFiles— Timeout cleanup races withfinalizeDownload. Between deciding a hash is "still missing" and runningdownloads.modify, a concurrent fiber could complete the download. This can leave inconsistent state where the file exists on disk but is in neitherdownloadsnorcompleted. -
FileRequester.scala— MutableBlake2bDigestobject is shared across atomicdownloads.modifyboundary via shallow.copy(). If a retry or concurrent operation touches the oldDownloadStatereference aftermodify, the digest corrupts silently.
Edge Cases
-
FileUploadAPI.scala:validateMetadata—maxFileSizeis checked only against the client-declaredmetadata.fileSize, not actual bytes received. A client can declare a valid size but stream more data. -
FileUploadAPI.scala:finalizeUpload— Atomic rename (.tmp→ final) and.meta.jsonwrite are two separate operations. JVM crash between them leaves a file without metadata sidecar, with no recovery path. -
Costs.scala:fileStorageCost—Math.multiplyExactthrowsArithmeticExceptionon overflow but callers don't catch it. At highphloPerStorageByteor large files, this propagates as an unhandled exception through the evaluator. -
FileReplicationSetup.scala:create— Falls back to relative pathPaths.get("file-replication")whenfileReplicationDirisNone. Location depends on JVM working directory — unpredictable in production. -
DeployGrpcServiceV1.scala:checkFileFinalized— FileRegistry contract address hardcoded as a string literal inexploratoryDeploy. If the address changes (upgrade, different shard), all downloads silently fail. -
FileDownloadAPI.scala:doStream—Files.size(filePath)called twice (once for logging, once for metadata chunk). File could be deleted between calls by orphan cleanup. Should capture size once. -
FileDownloadAPI.scala— No validation thatoffset < fileSize. An offset beyond EOF silently returns an empty data stream with a metadata chunk claiming the full file size. -
DeployServiceV1.proto:FileUploadMetadata.term— Client provides raw Rholang term. If the signed term isn't the canonicalrho:io:fileregistration, deploy enters mempool but never registers the file, creating an orphan with no cleanup trigger.
DRY
-
Hex regex
"^[a-f0-9]{64}$"hardcoded in 3 places.toHexconversion duplicated in 3 places. Both should be shared. -
File cleanup logic (
deleteIfExistsfor data +.meta.json) copy-pasted in two error branches withinDeployGrpcServiceV1.uploadFile. -
FileReplicationSetup.create+ destructure pattern repeated identically in 3 call sites (CasperLaunch,GenesisCeremonyMaster,Initializing).
Tests
FileDeploySelectionSpec:applyBackpressure— UsestakeWhile(stops at first over-budget), but productionBlockCreatorusesfoldLeft(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).
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
f2cd28d to
70d08db
Compare
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
uploadFileanddownloadFilegRPC streaming implementationIntegration 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
FileChannelwith directByteBufferallocation (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.tmpfile is immediately cleaned up.2. Automated Synthetic Deploy
After file ingestion, the node automatically constructs and submits a blockchain transaction (
file!("register", ...)on therho:io:filesystem channel) linking the file to the chain. The client makes ONE call — ifuploadFilesucceeds, the file is on disk AND the deploy is in the mempool. The response includes adeployIdfor lifecycle tracking via existingfindDeploy/isFinalizedRPCs.3.
FileRegistry.rho— On-Chain Ownership & DeduplicationA Rholang smart contract (modeled after
SystemVault.rho) that maintains a per-file registry map (fileHash → { deployers: {pk1: true, ...}, name: "file.ext" }). Provides: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 theFileRegistryat the Last Finalized Block post-state viaexploratoryDeploybefore serving bytes. Includes resume support viaoffsetfield.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) andmax-file-deploys-per-block(10).7. Storage-Proportional Phlo Pricing (Layer 5)
File uploads are charged
BASE_REGISTER_PHLO + fileSize × phloPerStorageBytewith enforcement at both upload-time (early rejection) and block-execution time (viaCostAccounting.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
deployLifespanwas increased from 50 to 250 blocks to prevent premature cleanup during large P2P replication.How It Works (High Level)
Architecture Documentation
For the full five-layer architecture specification, see
streaming_file_upload.md