Portaldot V3 ships a storage pallet that nobody had used.
PotDrive turns it into a drive: content-addressed, deduplicated, one signature to store, and verified by every reader.
The landing page follows one file from your disk to the chain and back, in 3D, as you scroll. Light and dark themes.
Portaldot V3 runs TransactionStorage, the pallet behind Polkadot's Bulletin chain. Any account can store data in it, block producers must keep proving they still hold that data, and the node serves it by content ID. When we checked chain state on 17 October 2026, it held zero entries. There was no SDK, no gateway and no app for it, and teams building on Portaldot were sending their files to outside IPFS pinning services.
PotDrive is the missing layer. Files are chunked by content (FastCDC), addressed with IPFS CIDs built on the exact hash Portaldot indexes, and stored with one wallet signature. Blocks the chain already holds are never paid for twice. Every reader (the Drive, the gateway, the SDK, the CLI and an MCP server for AI agents) re-hashes every block before trusting it.
| Live | potdrive.anshverma.tech: Drive, file pages, network explorer |
| Docs | potdrive.anshverma.tech/docs: 21 pages, from concepts to API reference |
| Network | Portaldot V3 testnet · wss://test-node-v3.portaldot.dev · EVM chain id 420420777 |
| Registry | PotDriveRegistry 0xdd3e268957ebeccfcca61b5676e31e620160d027 on Revive, deployed in block #89181 · tx 0x772bbc3de2241089ed9ffe21658d884ec07134b2e9abc79d3e71c97ab7c81f4e (extrinsic 89181-1). PotDriveCheck runs live through eth_call |
| Package | @potdrive/sdk with React bindings at @potdrive/sdk/react |
| Event | Track | Dates |
|---|---|---|
| Portaldot Hacker House 2026 | Modular Cloud & Middleware | Check-in Oct 24 · Final Nov 6 · Demo Day Nov 8–9 |
flowchart LR
subgraph Device["Your device"]
F[File or folder]
C[FastCDC chunks<br/>blake2b CIDs<br/>UnixFS DAG]
W[Wallet<br/>1 signature]
K[Upload key<br/>in this tab]
end
subgraph Portaldot["Portaldot V3"]
TS[(TransactionStorage<br/>held 100,800 blocks<br/>checkProof every block)]
BS[bitswap_v1_get]
R[(PotDriveRegistry<br/>Revive · Solidity)]
MT[MetaTx pallet]
NF[(Nfts pallet)]
end
subgraph Readers
G[Gateway /ipfs/cid]
D[Drive · file pages]
S[SDK · CLI · MCP]
end
F --> C
C -- "only blocks Portaldot doesn't hold" --> K
W -- "batchAll: fund key + register" --> R
W -. "fund" .-> K
K -- "store × n" --> TS
K -. "or signed meta-tx, relay pays" .-> MT --> TS
TS --> BS
BS -- "re-hash every block" --> G & D & S
W -- "mint" --> NF
- Prepare, on your device. The file is cut with FastCDC: boundaries follow the content, between 256 KiB and 1 MiB, averaging 512 KiB. Each chunk gets a CIDv1 whose multihash is
blake2b-256, the exact digest TransactionStorage indexes. Files bigger than one chunk get a UnixFSdag-pbroot. Folders become UnixFS directories. Private files are sealed with AES-256-GCM first. - Deduplicate against the chain. One state read lists every content hash Portaldot holds. Chunks it already has, from an older version, another file or another user, are linked instead of stored.
- Sign once. One
utility.batchAllfunds a throwaway upload key and records the file in the registry. The node indexes each extrinsic's tail as a single entry, so chunks can't share a batch. The upload key signs onetransactionStorage.storeper new chunk, with no further prompts. - Verify and return the change. The root comes back over
bitswap_v1_getand is re-hashed. Then the key sends its unspent budget home withbalances.transferAll. - Keep it alive. Portaldot drops data after 100,800 blocks (about 7 days) unless someone calls
renew. Prepaid keep-alive sits in the registry, and a daily keeper renews files before they expire. Anyone can fund any file.
sequenceDiagram
participant B as Browser
participant W as Wallet
participant K as Upload key
participant P as Portaldot
B->>B: FastCDC · blake2b CIDs · dag-pb root (no network)
B->>P: TransactionStorage.Transactions — what's already held?
B->>W: sign batchAll[transferKeepAlive(key), revive.call(register)]
W->>P: 1 extrinsic
loop each new chunk
K->>P: transactionStorage.store(chunk)
end
B->>P: bitswap_v1_get(root) → re-hash
K->>P: transferAll(owner)
Take any one of these out and PotDrive stops working.
| Runtime module | What it does in PotDrive |
|---|---|
TransactionStorage (store, renew, checkProof) |
Holds every chunk. Validators must prove a random 256-byte piece of each stored transaction one retention period later, or their block is invalid. |
Bitswap RPC (bitswap_v1_get) |
The read path for everything: Drive, gateway, SDK, CLI and MCP. It serves any CIDv1 whose multihash is a stored blake2b digest, so PotDrive CIDs are ordinary IPFS CIDs. |
Revive (instantiateWithCode, call, reviveApi) |
Runs PotDriveRegistry, plain EVM bytecode called from a Polkadot wallet in the same batch as the upload. It handles ownership, versions, prepaid keep-alive and sealed shares. |
Revive System precompile (hashBlake256, sr25519Verify) |
Lets the Solidity contracts hash with TransactionStorage's own blake2b and check Polkadot wallet signatures, so the registry proves files instead of trusting uploaders. |
Utility + Balances (batchAll, transferAll) |
Turn a many-transaction upload into one signature, and return the upload key's change. |
MetaTx + VerifySignature (dispatch) |
Gasless uploads: users with 0 tPOT sign once, and a relay pays for the meta-transactions. |
Nfts (create, mint, setMetadata) |
NFTs whose metadata and media both live on Portaldot. No collection existed on the V3 testnet before this. |
A file registry usually writes down whatever the uploader claims. PotDrive's doesn't: PotDriveRegistry receives the file's root block.
- Hash. It hashes the root block with Portaldot's blake2b through the System precompile, the exact hash TransactionStorage indexes data under.
- Decode. It parses the IPFS dag-pb / UnixFS protobuf in Solidity, and records the size and block count the bytes prove.
- Refuse forgeries. A forged root is refused on the live chain with
SizeMismatch(). - Gasless registration.
PotDrivePermitsrebuilds the readable text a Polkadot wallet showed, CID base32-encoded in Solidity, inside the<Bytes>wrapper. It checks the signature withsr25519Verify, so one wallet signature both sponsors the upload and registers the file. - Free verification.
PotDriveCheckis never deployed. Its creation code runs througheth_call, so anyone's browser can ask Portaldot's runtime to verify a file, for free. The landing page does exactly that.
Revive charges about 0.078 tPOT per byte of contract code, so the design is driven by bytecode size. The first all-in-one version was 12,165 B, a 952 tPOT deposit; the deployed core is 5,619 B, about 447 tPOT. See contracts/README.md and /docs/registry.
| Owner | Anyone with the link | A wallet you shared to | Everyone else | |
|---|---|---|---|---|
| Public file | ✓ | ✓ | ✓ | ✓ |
| Private file: name, type, content | ✓ | ✓ (key in #k=) |
✓ (key sealed to their inbox) | ✕ |
| Private file: that it exists, its size | ✓ | ✓ | ✓ | ✓ |
| Your upload key | used for one upload, then swept | ✕ | ✕ | ✕ |
The #k= fragment is never sent to a server: not to PotDrive, the gateway or the RPC node.
| Drive | Sections for My Drive, Recent, Websites, Private, Shared with me, NFTs and Expiring soon. Search by name or CID, sort, list or grid, a details panel, a right-click menu, and drag a file anywhere to upload. |
| Upload sheet | Shows what's new and what's already on chain, a fee estimate from paymentInfo, private on/off, keep-alive for 1, 5 or 13 weeks, and pay with your wallet or sponsored. |
| Upload panel | Starting an upload collapses it into a corner panel, Google Drive style, with live block-by-block progress. |
| Versions | Upload a new version of a file. Lists show the latest. The details panel shows v1, v2, v3. Each version only pays for changed chunks. |
| Private files | AES-256-GCM on your device. The name and type are sealed too. The key lives only in the link fragment. |
| Share with a wallet | Seal a private file's key to another address's X25519 inbox key. It shows up under their Shared with me. |
| NFTs | Mint any image on Portaldot's native Nfts pallet in one signature, and browse your NFTs in the Drive. |
| Websites | Upload a folder or run potdrive deploy ./dist. The gateway serves it at /ipfs/<cid>/ in a sandboxed origin. |
| File pages | A big preview, plus a "Verified on this device" grid that fills block by block straight from the Portaldot node. Retention ring, details, and a button to fund keep-alive. |
| Explore | Everything TransactionStorage holds right now on a retention timeline, plus a storage-proof monitor for recent blocks. |
Storage on the V3 testnet costs about 105 tPOT per MiB, so re-storing unchanged bytes is the most expensive mistake a storage layer can make. With fixed-size chunks, one inserted byte shifts every later boundary. FastCDC (Xia et al., USENIX ATC 2016) with normalized chunking keeps boundaries anchored to content. Measured on a 16 MiB file (bun scripts/bench-cdc.ts):
| Edit | Fixed 1 MiB: new bytes | FastCDC: new bytes |
|---|---|---|
| Insert 1 KiB at 300 KiB | 16,385 KiB (100%) | 609 KiB (3.7%) |
| Delete 10 KiB at 5 MiB | 11,254 KiB (68.7%) | 343 KiB (2.1%) |
| Overwrite 4 KiB mid-file | 1,024 KiB (6.3%) | 898 KiB (5.5%) |
| Append 2 MiB | 2,048 KiB (11.1%) | 2,513 KiB (13.6%) |
- Chunks can't share a batch.
sc-client-db::apply_index_opskeeps one index entry per extrinsic and stores the encoded extrinsic's lastsizebytes, hence the in-tab upload key. - Bytes must be passed as hex. polkadot.js reads a raw
Uint8Arraypassed asBytesas already length-prefixed. Passed raw,store()would index the wrong data and fee estimates would price empty payloads. This was caught by a test before any tPOT was spent. - EVM constructors on Revive. Arguments go on the end of the init code, and
datamust be empty, or you getEvmConstructorNonEmptyData(Revive error 54). - Deposits shape the contract. Code costs about 0.078 tPOT per byte to deploy, measured by dry run. Read-only checks moved to
eth_call, permits became an optional module, the IR optimizer's duplicated inlining was removed by using one entry point, and precompile calls are hand-assembled: 12,165 B (952 tPOT) down to 5,619 B (447 tPOT). - Yul evaluates arguments right to left.
and(staticcall(…), eq(mload(0), 1))reads the result before the call runs; the call must be its own statement. - No contract creation inside a dry run. Revive here refuses
CREATEfrom a contract duringeth_callorreviveApi.instantiate, so live checks run each contract on its own. - MetaTx signing. The meta-tx pipeline is
(VerifySignature, MetaTxMarker, CheckNonZeroSender, CheckSpecVersion, CheckTxVersion, CheckGenesis, CheckMortality, CheckNonce, CheckMetadataHash), and the signature coversblake2_256(version ‖ call ‖ explicit ‖ implicit). Browser wallets wrap raw messages in<Bytes>, so the wallet signs a readable authorization and the upload key signs the meta-txs.
bun test sdk: 39 tests.- CIDs match
blake2b-256exactly as TransactionStorage indexes it. - DAGs and directories are deterministic, with sorted links and path-traversal rejection.
- FastCDC respects its bounds and re-uses chunks after edits where fixed chunking doesn't.
- The private envelope round-trips, and a wrong key fails cleanly.
- Wallet shares open only for the right inbox and are bound to their file root.
- Sponsor authorizations accept raw and wallet-wrapped signatures and reject a tampered budget.
- NFT metadata stays under the 256-byte limit.
- Permit text matches the contract, signatures cover every field, and a tampered budget fails.
- CIDs match
sdk/test/read.test.tsruns the entire read path against a fake Portaldot node. That covers multi-chunk reassembly, range reads fetching only the needed chunks, stitching across chunk boundaries, refusing tampered bytes, and resolving folder paths.cd contracts && forge test: 32 tests, run against fixtures the SDK generates. A stand-in precompile computes real blake2b on the EVM's BLAKE2 F precompile, so local hashes equal Portaldot's. The tests cover:- UnixFS proofs, including forged sizes, non-blake2b links and unsorted directories;
- wallet permits: the text matches the SDK, there's no replay, every field is bound, and permits expire;
- keeper caps and timing;
- ownership, sharing and admin.
bun scripts/onchain-check.ts: 6 checks against Portaldot itself, througheth_call:- blake2b and UnixFS decode of a real 3 MiB file;
- a website directory;
- a raw block hashed by the runtime;
- a forged root refused;
- a real sr25519 wallet signature verified, and a tampered permit rejected.
- Gateway end to end (
scripts/fake-node.tsplusPOTDRIVE_RPC_URL):- The directory redirect works and serves
index.html. 206partial responses are byte-exact, including suffix ranges, plus416,404and downloads.- A stored site runs with an opaque origin (
window.origin === "null"), and itslocalStorageaccess throwsSecurityError.
- The directory redirect works and serves
- Against the live testnet:
- Fees measured with
paymentInfo. - Registry, permit and verifier deploys dry-run through
reviveApi.instantiate, with their deposits measured. - The address rule (
keccak256(publicKey)[12..]) matchesreviveApi.address. - Meta-transactions encode, decode and recover their signer.
- The proof monitor reads real blocks.
- Fees measured with
bun scripts/ui/readme-shots.tsretakes every image in this README with Playwright, and fails on any page error.
On chain (Portaldot V3 testnet):
| What | Where |
|---|---|
PotDriveRegistry deployed |
0xdd3e268957ebeccfcca61b5676e31e620160d027, block #89181, tx 0x772bbc3de2241089ed9ffe21658d884ec07134b2e9abc79d3e71c97ab7c81f4e (revive.instantiateWithCode, signed from a browser wallet at /deploy) |
The first real upload and the gasless module (PotDrivePermits) are next.
sdk/src/ TypeScript SDK
cdc.ts FastCDC (normalized), fixed gear table
cid.ts dag.ts blake2b CIDs, UnixFS files and directories
upload.ts one-signature upload, chain-level dedup, upload key, sweep
fetch.ts verified reads, range reads, path resolution
metatx.ts MetaTx payloads; sponsored.ts: gasless upload client
share.ts X25519 + HKDF + AES-GCM sealing to inbox keys
nft.ts native Nfts pallet minting
registry.ts Revive registry reads (eth_call, eth_getLogs) and calls
renew.ts keeper.ts proofs.ts state.ts
sdk/react/ usePotFile, <PotImage>, <PotVideo>
permit.ts readable sr25519 permits, mirrored byte for byte by the contract
onchain.ts free on-chain verification: PotDriveCheck through eth_call
sdk/test/ 39 tests, including a fake-node read path
contracts/ PotDriveRegistry, PotDrivePermits, PotDriveVerifier/Check + 32 Foundry tests
src/app/ Next.js: landing, /drive, /f/[cid], /explore, /docs (21 pages)
src/app/ipfs/ IPFS-style gateway with Range, directories, sandboxing
src/app/api/ file metadata, chain state, proofs, keeper cron, MetaTx relay
cli/potdrive.ts put · get · deploy · info · renew · ls
mcp/server.ts MCP server for AI agents
scripts/ deploy, live chain checks, ABI + fixture generators, CDC benchmark, fake node, screenshots
bun install
bun run dev # http://localhost:3000 (/drive?simulate replays an upload, dev only)
# checks
bun test sdk
cd contracts && forge test && cd ..
bun scripts/onchain-check.ts # the contracts' checks, run by Portaldot itself (free)
bun scripts/bench-cdc.ts
# CLI
export POTDRIVE_SEED="twelve words …"
bun cli/potdrive.ts put ./deck.pdf --private
bun cli/potdrive.ts deploy ./dist
bun cli/potdrive.ts get <cid> -o deck.pdf
# deploy the registry (≈ 447 tPOT refundable code deposit); --permits adds gasless registration
POTDRIVE_SEED="…" KEEPER_SEED="…" bun scripts/deploy.tsSite environment: NEXT_PUBLIC_REGISTRY, NEXT_PUBLIC_REGISTRY_BLOCK, NEXT_PUBLIC_PERMITS, KEEPER_SEED, CRON_SECRET, RELAYER_SEED, RELAY_MAX_BYTES.
- Testnet only. Storage costs about 105 tPOT per MiB on the V3 testnet, mostly the length fee.
- Retention, not permanence. Data lasts 100,800 blocks per payment. Keep-alive automates renewals, but someone has to fund them.
- One RPC is trusted for availability and chain state. Integrity is checked by every reader. A light client would remove the rest.
- The keeper is a trusted operator. Its claims are capped per file and public, but the contract can't see native-side renewals.
- Inbox keys live in one browser. Losing one loses access to files shared with you, not your own uploads.
MIT licensed.










