<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Levai!</title>
    <description>The latest articles on Forem by Levai! (@levai).</description>
    <link>https://forem.com/levai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1128228%2Fa03469e2-4e0d-4797-a48f-c32fbb31a13b.png</url>
      <title>Forem: Levai!</title>
      <link>https://forem.com/levai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/levai"/>
    <language>en</language>
    <item>
      <title>Storage solutions with Midnight - handling off-chain data with privacy</title>
      <dc:creator>Levai!</dc:creator>
      <pubDate>Thu, 02 Apr 2026 10:23:53 +0000</pubDate>
      <link>https://forem.com/levai/storage-solutions-with-midnight-handling-off-chain-data-with-privacy-2mm9</link>
      <guid>https://forem.com/levai/storage-solutions-with-midnight-handling-off-chain-data-with-privacy-2mm9</guid>
      <description>&lt;p&gt;Blockchain storage is a premium resource. While storing data directly on the ledger ensures immutability, it is often prohibitive for large files and can compromise user privacy if sensitive information is leaked.&lt;/p&gt;

&lt;p&gt;On the Midnight network, storing large files directly on-chain is impractical and expensive. This is where off-chain storage comes in. You store your data off-chain, then store the proof of that data on-chain.&lt;/p&gt;

&lt;p&gt;In this guide, you will learn how to integrate off-chain storage solutions like IPFS (InterPlanetary File System) or Arweave with Midnight smart contracts. You will build a document management system that encrypts files before upload, stores them off-chain, and uses Midnight's shielded state to manage access and verify data integrity.&lt;/p&gt;

&lt;p&gt;By the end of this guide, you will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understand the strategic trade-off between on-chain and off-chain storage and when to use both.&lt;/li&gt;
&lt;li&gt;Learn how to use content hashes and cryptographic commitments to verify data integrity.&lt;/li&gt;
&lt;li&gt;Encrypt sensitive data using AES-256-GCM before uploading to IPFS or Arweave.&lt;/li&gt;
&lt;li&gt;Implement access control using derived viewing keys and hybrid encryption.&lt;/li&gt;
&lt;li&gt;Build and deploy a complete document management system on Midnight.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before starting this tutorial, ensure you have the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js: v22+&lt;/li&gt;
&lt;li&gt;NPM&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.midnight.network/next/getting-started/installation#install-compact" rel="noopener noreferrer"&gt;Compact Compiler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://app.pinata.cloud/" rel="noopener noreferrer"&gt;Pinata Account&lt;/a&gt; for IPFS storage.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chromewebstore.google.com/detail/lace/gafhhkghbfjjkeiendhlofajokpaflmk" rel="noopener noreferrer"&gt;Lace Wallet&lt;/a&gt; (for preprod; not required for local development)&lt;/li&gt;
&lt;li&gt;Basic knowledge of TypeScript and Compact.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  On-chain vs. off-chain storage
&lt;/h2&gt;

&lt;p&gt;As with building anything, before you begin writing code, you must first decide where your data lives. There are two options for storing data: on-chain and off-chain. These are not interchangeable as they serve fundamentally different purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On-chain storage&lt;/strong&gt; refers to the data storage system on the ledger itself. The ledger provides an immutable and privacy-preserving state; it is not designed to hold high-volume data like PDFs, high-resolution images, or massive data records.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Off-chain storage&lt;/strong&gt;, on the other hand, refers to any system that exists outside the ledger. Instead of overloading the ledger with large files and incurring massive costs, you store the files in an external system (like IPFS) and store a small pointer or "commitment" on-chain. This pointer serves to prove that the off-chain data exists and has not been tampered with.&lt;/p&gt;

&lt;p&gt;To determine what belongs on-chain, follow this simple rule:&lt;br&gt;
"If the data is essential for the smart contract to function, then it belongs on-chain."&lt;/p&gt;

&lt;p&gt;Such data includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ownership state (Who owns the document?)&lt;/li&gt;
&lt;li&gt;Cryptographic commitments (Hashes of the content)&lt;/li&gt;
&lt;li&gt;Metadata required for zero-knowledge (ZK) proofs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else belongs off-chain. This includes raw files like PDFs or images, large bodies of text, and sensitive records that only require verification or retrieval rather than on-chain computation.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criteria&lt;/th&gt;
&lt;th&gt;On-Chain&lt;/th&gt;
&lt;th&gt;Off-Chain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Use Case&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Access rules, ownership proofs, commitments&lt;/td&gt;
&lt;td&gt;Raw files, large data, retrievable records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Capacity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Limited (kilobytes)&lt;/td&gt;
&lt;td&gt;Unlimited (gigabytes+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Privacy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Selective disclosure&lt;/td&gt;
&lt;td&gt;Encrypted before upload for privacy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Persistence&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Permanent, immutable&lt;/td&gt;
&lt;td&gt;Depends on provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Consensus required&lt;/td&gt;
&lt;td&gt;Immediate retrieval&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Building the document manager
&lt;/h2&gt;

&lt;p&gt;Now that you understand what belongs on-chain and what belongs off-chain, it is time to build. You will create a system where file metadata is shielded on Midnight, while the files themselves are encrypted and stored off-chain.&lt;/p&gt;

&lt;p&gt;A repository has been prepared with the complete code for this project. This guide walks through the implementation of off-chain storage as demonstrated in the repository.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: Project setup
&lt;/h3&gt;

&lt;p&gt;First, clone the repository and install dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Mackenzie-OO7/midnight-doc-manager.git
&lt;span class="nb"&gt;cd &lt;/span&gt;midnight-doc-manager
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Configure the environment
&lt;/h4&gt;

&lt;p&gt;Configure the connection to the storage provider (Pinata) and the Midnight network.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Copy the example configuration file&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Setting up Pinata (IPFS):
&lt;/h5&gt;

&lt;ol&gt;
&lt;li&gt;Log in to your Pinata Dashboard (&lt;a href="https://app.pinata.cloud" rel="noopener noreferrer"&gt;https://app.pinata.cloud&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;In the sidebar on your left under the "Developer" section, select API Keys and click New Key.&lt;/li&gt;
&lt;li&gt;Input a name for your new API key and enable the admin checkbox. Then click Create.&lt;/li&gt;
&lt;li&gt;Copy the JWT (the long string) and replace the placeholder value for &lt;code&gt;PINATA_JWT&lt;/code&gt; in your .env file.&lt;/li&gt;
&lt;li&gt;Navigate to “Gateways” in your Pinata Dashboard. Copy your dedicated gateway URL (it looks like ‘&lt;a href="https://your-name.mypinata.cloud%E2%80%99" rel="noopener noreferrer"&gt;https://your-name.mypinata.cloud’&lt;/a&gt;) and set it as &lt;code&gt;PINATA_GATEWAY&lt;/code&gt; in your .env file.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 2: Understanding the smart contract
&lt;/h3&gt;

&lt;p&gt;Head over to &lt;code&gt;contracts/document-manager.compact&lt;/code&gt;. This file defines the rules of the system. It specifies what data is stored, and who can change it.&lt;/p&gt;

&lt;p&gt;The contract handles three critical things:&lt;/p&gt;

&lt;h5&gt;
  
  
  Storing document metadata
&lt;/h5&gt;

&lt;p&gt;The &lt;code&gt;DocumentRecord&lt;/code&gt; struct links the off-chain file to its on-chain proof.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;DocumentRecord&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;contentHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// SHA-256 of the ORIGINAL file&lt;/span&gt;
        &lt;span class="n"&gt;storageCid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Opaque&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// IPFS Address (encrypted content)&lt;/span&gt;
        &lt;span class="n"&gt;ownerCommitment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// Proof of ownership (hidden)&lt;/span&gt;
        &lt;span class="n"&gt;fileType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Opaque&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// MIME type (e.g., "application/pdf")&lt;/span&gt;
        &lt;span class="n"&gt;isActive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Boolean&lt;/span&gt;             &lt;span class="c1"&gt;// For "soft deletes"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Proving integrity
&lt;/h5&gt;

&lt;p&gt;The &lt;code&gt;verifyDocument&lt;/code&gt; circuit checks if a provided file hash matches the one stored on the ledger. This allows anyone to verify a file's authenticity without trusting the storage provider.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;    &lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;verifyDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;providedHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="nf"&gt;.lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documentId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="py"&gt;.contentHash&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;providedHash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="py"&gt;.isActive&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Managing access control
&lt;/h5&gt;

&lt;p&gt;The &lt;code&gt;AccessGrant&lt;/code&gt; struct allows secure sharing. It stores an encrypted key that only the intended recipient can unlock.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;AccessGrant&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;encryptedKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Opaque&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Opaque&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;senderPublicKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Opaque&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The contract relies on the following components to manage state and privacy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ledger&lt;/strong&gt;: The &lt;code&gt;documents&lt;/code&gt; map stores records, and &lt;code&gt;accessGrants&lt;/code&gt; stores permissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Witness Function&lt;/strong&gt;: &lt;code&gt;witness secretKey(): Bytes&amp;lt;32&amp;gt;&lt;/code&gt;. It allows your local wallet to supply a secret key to the contract for zero-knowledge proof generation, without ever revealing that key to the network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Circuits&lt;/strong&gt;: The contract defines the following actions:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;registerDocument&lt;/code&gt;: Creates a new immutable record linked to the owner's private key.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;updateDocument&lt;/code&gt;: Allows the owner to update the file hash or storage location.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;deactivateDocument&lt;/code&gt;: Enables the owner to soft-delete a document, marking it as inactive.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;verifyDocument&lt;/code&gt;: Publicly proves that a provided file matches the on-chain record without revealing the owner.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grantAccess&lt;/code&gt;: Securely stores an encrypted key for a specific recipient.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;revokeAccess&lt;/code&gt;: Removes a shared key, effectively revoking access for that user.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hasAccess&lt;/code&gt;: Checks if an access grant exists for a given recipient.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getAccessGrant&lt;/code&gt;: Retrieves the encrypted key for an authorised recipient.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getDocument&lt;/code&gt;: Retrieves the full document record by ID.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h4&gt;
  
  
  Compile the smart contract
&lt;/h4&gt;

&lt;p&gt;Next, compile the contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    npm run compile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates the necessary TypeScript bindings and stores them in a new sub-directory contracts/managed/document-manager/.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: The application layer
&lt;/h3&gt;

&lt;p&gt;Now that you have the smart contract, you need to build the application layer to interact with it.&lt;/p&gt;

&lt;p&gt;From what is defined in the smart contract, there are three distinct features to implement:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Encryption&lt;/strong&gt;: First, secure the data itself. Create utilities to encrypt files client-side using AES-256-GCM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key management&lt;/strong&gt;: Second, handle the keys. Implement key derivation to generate specific keys for each user and document.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt;: Finally, store the data. Create providers for IPFS and Arweave to hold the encrypted data blobs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's begin with encryption.&lt;/p&gt;

&lt;h4&gt;
  
  
  Encryption utility
&lt;/h4&gt;

&lt;p&gt;If you simply uploaded files to IPFS, they would be public, and anyone with the CID could access them, including the storage provider. To create a private document manager, you must encrypt your data before it ever leaves the user's device.&lt;/p&gt;

&lt;p&gt;The AES-256-GCM standard is widely considered the gold standard for symmetric encryption. It provides confidentiality, making the data impossible to read without the key, and integrity via an authentication tag. If even a single bit of the encrypted file is altered on IPFS, decryption fails instantly.&lt;/p&gt;

&lt;p&gt;Here is the core logic from &lt;code&gt;src/utils/encryption.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;encryptFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;EncryptedData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 1. Validate the key length&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Encryption key must be 32 bytes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 2. Generate a unique IV (Initialisation Vector)&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 3. Create the cipher and encrypt the data&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createCipheriv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aes-256-gcm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileData&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;final&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;

        &lt;span class="c1"&gt;// 4. Return ciphertext, IV, and Auth Tag (Integrity Proof)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;authTag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuthTag&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uploading to IPFS packs these three components: &lt;code&gt;iv&lt;/code&gt;, &lt;code&gt;authTag&lt;/code&gt;, and &lt;code&gt;ciphertext&lt;/code&gt; into a single binary blob. This ensures the downloader has everything needed to verify and decrypt the file, except the key.&lt;/p&gt;

&lt;h4&gt;
  
  
  Key management in Midnight
&lt;/h4&gt;

&lt;p&gt;If I encrypt a file with a key on my machine, how do I let others read it without managing thousands of loose keys? And how do I prove ownership without exposing my wallet history?&lt;/p&gt;

&lt;p&gt;The solution is a two-part key strategy. First, derive a deterministic key from your wallet seed for ownership proofs on-chain. Then generate an independent encryption key pair for sharing files securely.&lt;/p&gt;

&lt;h5&gt;
  
  
  A. The contract witness key
&lt;/h5&gt;

&lt;p&gt;This key is derived using a SHA-256 hash of your wallet seed and a unique prefix. It serves as a private input (witness) for zero-knowledge proofs, allowing you to prove you own a document without revealing your identity. Below is the derivation from &lt;code&gt;src/api/contract.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// src/api/contract.ts — inside the connect() method&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secretKeyBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;midnight-doc-manager:owner-key:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The witness function then supplies this key to the contract at proof generation time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// src/api/witnesses.ts&lt;/span&gt;
    &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createWitnesses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Secret key must be 32 bytes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WitnessContext&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DocumentManagerPrivateState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;DocumentManagerPrivateState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;privateState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To prove ownership without revealing the key itself, the application computes an ownership commitment using Midnight's &lt;code&gt;persistentHash&lt;/code&gt;  and not plain SHA-256, so the result matches the on-chain check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;computeOwnerCommitment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Secret key must be 32 bytes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;persistentHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BYTES32_DESCRIPTOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  B. The encryption identity key (viewing key)
&lt;/h5&gt;

&lt;p&gt;This is your identity for file sharing. Unlike the witness key, it is not derived from your wallet seed. It is an independent X25519 keypair generated randomly via &lt;code&gt;nacl.box.keyPair()&lt;/code&gt; and stored locally in &lt;code&gt;.midnight-doc-keys.json&lt;/code&gt;. It has nothing to do with your mnemonic or wallet address. Your public key can be distributed so that others can encrypt files for you, while your private key remains secret and is used only to unwrap and decrypt keys shared with you. If you lose it, you cannot decrypt documents you have uploaded or that have been shared with you.&lt;/p&gt;

&lt;h5&gt;
  
  
  Why derive separate keys?
&lt;/h5&gt;

&lt;p&gt;Ideally, you never want to use your raw wallet seed directly in an app. If that seed is compromised, your entire wallet is at risk. By deriving specific keys for specific tasks, you ensure that even if the app's keys are compromised, your funds and other DApps remain secure&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftu23r7mwxjlj1yi1ilqd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftu23r7mwxjlj1yi1ilqd.png" alt="wallet seed to file key flow" width="742" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  The sharing mechanism (key wrapping)
&lt;/h5&gt;

&lt;p&gt;Now, how do we share files? You don't actually share the file itself. The file is already on IPFS and anyone can download the encrypted blob. What you need to share is the key to decrypt it.&lt;/p&gt;

&lt;p&gt;Key Wrapping securely sends this key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consider this scenario&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Alice (Sender) has the document, which is encrypted with a random AES Key; let's call it the File Key.&lt;/li&gt;
&lt;li&gt;Alice wants to share the File Key with Bob. She fetches Bob's Encryption Public Key.&lt;/li&gt;
&lt;li&gt;Alice uses her Encryption Private Key and Bob's Encryption Public Key to generate a secure shared secret.&lt;/li&gt;
&lt;li&gt;Alice uses this shared secret to encrypt the File Key. This encrypted key is called the Wrapped Key.&lt;/li&gt;
&lt;li&gt;Alice stores this Wrapped Key on the Midnight contract.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt;&lt;br&gt;
Bob can download the Wrapped Key from the contract. He uses his Encryption Private Key to "unwrap" it, revealing the File Key, which he then uses to decrypt the actual file from IPFS.&lt;/p&gt;
&lt;h4&gt;
  
  
  The storage layer
&lt;/h4&gt;

&lt;p&gt;So far, you have implemented secure encryption to ensure files cannot be viewed publicly, and you have created viewing keys to grant read access to authorised parties. However, something important is missing: where do you actually store these files?&lt;/p&gt;

&lt;p&gt;When it comes to decentralised storage, two solutions lead the way: IPFS and Arweave.&lt;/p&gt;
&lt;h5&gt;
  
  
  IPFS
&lt;/h5&gt;

&lt;p&gt;IPFS allows you to store files using content addressing. Instead of a location (like myserver.com/file.pdf), the address is a hash of the content (the CID). If the file changes, the CID changes. This makes IPFS perfect for verification: store the CID on-chain. If the data on IPFS is tampered with, the CID changes immediately, alerting you to the discrepancy.&lt;/p&gt;
&lt;h5&gt;
  
  
  Arweave
&lt;/h5&gt;

&lt;p&gt;Arweave offers permanent storage. You pay once, and the data is stored forever on the "blockweave." This is ideal for critical documents like legal contracts or deeds that must never be deleted.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;src/storage.ts&lt;/code&gt;, implement a flexible &lt;code&gt;StorageProvider&lt;/code&gt; interface that supports both IPFS and Arweave so you can switch between them seamlessly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;StorageProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;storageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;getGatewayUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;storageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;?(&lt;/span&gt;&lt;span class="nx"&gt;storageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Deployment
&lt;/h3&gt;

&lt;p&gt;Finally, let's put it all together. A CLI has been included in the repository to help you deploy and test the application.&lt;/p&gt;

&lt;p&gt;You can either deploy locally via docker, or to the public preprod testnet.&lt;/p&gt;

&lt;p&gt;Note that the mnemonic phrase you use for one network cannot be used on the other. Each network is separate and requires its own funded wallet.&lt;/p&gt;

&lt;h4&gt;
  
  
  Option A: The local network
&lt;/h4&gt;

&lt;p&gt;The local network runs entirely in Docker and includes the node, indexer, and proof server. You will need two terminals open.&lt;/p&gt;

&lt;p&gt;In terminal 1,  start the local network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    git clone https://github.com/midnightntwrk/midnight-local-dev
    &lt;span class="nb"&gt;cd &lt;/span&gt;midnight-local-dev
    npm &lt;span class="nb"&gt;install
    &lt;/span&gt;npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the network is up and running, you will see the genesis master wallet logged. This is the network's internal funding source, not your wallet. You will then see the funding menu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  [1] Fund accounts from the config file (NIGHT + DUST registration)
  [2] Fund accounts by public key (NIGHT transfer only)
  [3] Display wallets
  [4] Exit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Select [1]. When prompted for a path, enter &lt;code&gt;./accounts.json&lt;/code&gt;. The repo ships with an accounts.json containing a pre-configured development wallet. You can use the mnemonic already there or replace it with your own. Note the mnemonic down as you will need it in Terminal 2.&lt;/p&gt;

&lt;p&gt;Leave Terminal 1 running once funding is complete. Navigate to Terminal 2, where you will deploy the contract:&lt;/p&gt;

&lt;p&gt;First, confirm the wallet was funded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
    npm run check-balance “&amp;lt;YOUR_MNEMONIC_PHRASE&amp;gt;”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a non-zero NIGHT and DUST balance. Then deploy the contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    npm run deploy “&amp;lt;YOUR_MNEMONIC_PHRASE&amp;gt;”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On success, the contract address is printed and saved to &lt;code&gt;deployment.json&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Option B: Preprod testnet
&lt;/h4&gt;

&lt;p&gt;Preprod is Midnight's public testnet. You will need two terminals open. Ensure that you are in the &lt;code&gt;midnight-doc-manager&lt;/code&gt; directory on both terminals.&lt;/p&gt;

&lt;p&gt;In terminal 1,  start the proof server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    npm run proof-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In terminal 2, deploy the contract:&lt;/p&gt;

&lt;p&gt;First, set .env to target preprod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    &lt;span class="nv"&gt;MIDNIGHT_NETWORK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;preprod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, create a new Lace wallet and select 'Preprod' in Settings → Network. Lace will show you a 24-word mnemonic when you create the wallet. Save it, as this is what you will pass to all CLI commands.&lt;/p&gt;

&lt;p&gt;Get your unshielded address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    npm run check-balance “&amp;lt;YOUR_MNEMONIC_PHRASE&amp;gt;”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get tNIGHT from the Midnight &lt;a href="https://midnight.network/faucet" rel="noopener noreferrer"&gt;faucet&lt;/a&gt; by pasting your unshielded address. Wait for the transaction to confirm, then run the &lt;code&gt;check-balance&lt;/code&gt; command again to verify the funds arrived and that DUST is non-zero before continuing.&lt;/p&gt;

&lt;p&gt;Deploy the contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    npm run preprod “&amp;lt;YOUR_MNEMONIC_PHRASE&amp;gt;”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deployment takes 60-120 seconds on preprod while waiting for block confirmations. On success, the contract address is saved to deployment.json.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the document manager
&lt;/h2&gt;

&lt;p&gt;Once the contract is deployed, you can upload, retrieve, share, and verify documents. All commands use the same mnemonic you used to deploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Generate an encryption keypair
&lt;/h3&gt;

&lt;p&gt;Create the keys you will use to share documents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    npm run cli &lt;span class="nt"&gt;--&lt;/span&gt; keys generate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This saves a keypair to .midnight-doc-keys.json in the current directory and prints your public key. If you lose this file, you will not be able to decrypt documents you have uploaded.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Upload a document
&lt;/h3&gt;

&lt;p&gt;With the smart contract deployed, you can now upload your first document. Create a test file and upload it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    &lt;span class="nb"&gt;echo&lt;/span&gt; “Midnight is great.” &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; demo.txt
    npm run cli &lt;span class="nt"&gt;--&lt;/span&gt; upload demo.txt “&amp;lt;YOUR_MNEMONIC_PHRASE&amp;gt;”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command encrypts the file with AES-GCM, uploads the encrypted blob to IPFS, and records the content hash and ownership proof on the Midnight smart contract.&lt;/p&gt;

&lt;p&gt;When the upload completes, the CLI prints a document ID. Save this value as you will need it for all subsequent commands.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Verify integrity
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    npm run cli &lt;span class="nt"&gt;--&lt;/span&gt; verify demo.txt &amp;lt;DOCUMENT_ID&amp;gt; “&amp;lt;YOUR_MNEMONIC_PHRASE&amp;gt;”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI computes the SHA-256 hash of the local file and asks the smart contract to verify that the hash matches the immutable record on the ledger. A match proves the file is completely identical to what was uploaded.&lt;/p&gt;

&lt;p&gt;For the full CLI reference, including download, file sharing, and access revocation, see the &lt;a href="https://github.com/Mackenzie-OO7/midnight-doc-manager/blob/main/README.md" rel="noopener noreferrer"&gt;README&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this guide, you learned how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Distinguish between on-chain and off-chain storage and decide where your data belongs.&lt;/li&gt;
&lt;li&gt;Encrypt files client-side using AES-256-GCM before uploading to decentralised storage.&lt;/li&gt;
&lt;li&gt;Derive deterministic keys from a wallet seed for ownership proofs and secure sharing.&lt;/li&gt;
&lt;li&gt;Use key wrapping (hybrid encryption) to share encrypted files with other users.&lt;/li&gt;
&lt;li&gt;Build storage providers for IPFS and Arweave.&lt;/li&gt;
&lt;li&gt;Deploy a Midnight smart contract that manages document metadata and access control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, try implementing off-chain storage in your own Midnight projects. Experiment with the provided code examples and adapt the patterns for your specific use cases. Feel free to share your implementations and challenges in the &lt;a href="https://forum.midnight.network/" rel="noopener noreferrer"&gt;Midnight forum&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Related resources:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.midnight.network/develop/reference/compact" rel="noopener noreferrer"&gt;Compact Language Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.ipfs.tech" rel="noopener noreferrer"&gt;IPFS Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.arweave.org" rel="noopener noreferrer"&gt;Arweave Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>zk</category>
      <category>midnightnetwork</category>
      <category>zeroknowledge</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
