Null Knowledge Assets (NKAs)
Null Knowledge Assets (NKAs) are the foundational primitive of NullGraph. Each NKA is a permanent, on-chain record of a negative scientific result stored as a Solana Program Derived Address (PDA).
95% of null results never get published. NKAs solve publication bias by making negative results permanent, discoverable, and economically valuable.
What is an NKA?
An NKA is a structured data account containing:
Hypothesis — the original hypothesis tested
Methodology — experimental methodology summary
Expected vs. Actual Outcome — what was expected and what actually happened
Statistical Data — p-value and sample size
Data Hash — SHA-256 fingerprint of the underlying dataset
Specimen ID — unique sequential identifier (e.g., NKA-0042)
Researcher — wallet address of the submitter
Status — verification state (Pending, Verified, Disputed)
Account Structure
Each NKA is stored in a NullResult account on Solana:
pub struct NullResult {
pub researcher : Pubkey , // Submitting wallet
pub specimen_number : u64 , // Sequential ID (NKA-0001, NKA-0002...)
pub hypothesis : [ u8 ; 128 ], // Hypothesis tested (UTF-8, zero-padded)
pub methodology : [ u8 ; 128 ], // Methodology summary
pub expected_outcome : [ u8 ; 128 ], // What was expected
pub actual_outcome : [ u8 ; 128 ], // What actually happened
pub p_value : u32 , // Fixed-point (8700 = 0.8700)
pub sample_size : u32 , // Sample size n
pub data_hash : [ u8 ; 32 ], // SHA-256 of attached data file
pub status : u8 , // 0 = Pending, 1 = Verified, 2 = Disputed
pub created_at : i64 , // Unix timestamp
pub bump : u8 , // PDA bump seed
}
All NKA accounts are program-owned PDAs derived from: seeds: ["null_result", researcher_pubkey, specimen_number_le_bytes]
Specimen Identifiers
Each NKA receives a unique sequential identifier from the global ProtocolState counter:
First submission: NKA-0001
Second submission: NKA-0002
And so on…
The counter is auto-incremented atomically during submission:
pub fn submit_null_result ( ctx : Context < SubmitNullResult >, ... ) -> Result <()> {
let state = & mut ctx . accounts . protocol_state;
state . nka_counter += 1 ;
let specimen_number = state . nka_counter;
let nr = & mut ctx . accounts . null_result;
nr . specimen_number = specimen_number ;
// ... store other fields
}
Specimen numbers are globally unique and permanent . They provide a universal reference system for null results across the entire protocol.
Data Hashing & Integrity
NKAs use SHA-256 cryptographic hashing to link on-chain metadata to off-chain datasets without storing the raw data on-chain.
How it works
Researcher uploads data file through the frontend (CSV, Excel, JSON, etc.)
Client-side hashing via Web Crypto API computes SHA-256 digest
Hash stored on-chain in the data_hash field (32 bytes)
File stays off-chain (IPFS, Arweave, or researcher’s own storage)
// Frontend hashing example
const hashFile = async ( file : File ) : Promise < Uint8Array > => {
const buffer = await file . arrayBuffer ();
const hashBuffer = await crypto . subtle . digest ( 'SHA-256' , buffer );
return new Uint8Array ( hashBuffer );
};
Verification Flow
Original Submission Researcher uploads experiment.csv → SHA-256: a3c8f...91b2
Later Verification Anyone can download the file and verify: sha256(experiment.csv) == a3c8f...91b2
The data hash provides a tamper-proof link between the on-chain record and the underlying dataset. If even one byte changes, the hash will not match.
All text fields use fixed-size byte arrays with zero-padding:
Field Size Format hypothesis128 bytes UTF-8, zero-padded methodology128 bytes UTF-8, zero-padded expected_outcome128 bytes UTF-8, zero-padded actual_outcome128 bytes UTF-8, zero-padded
Encoding Example
// Convert string to fixed-size byte array
const encodeString = ( str : string , size : number ) : Uint8Array => {
const buffer = new Uint8Array ( size );
const bytes = new TextEncoder (). encode ( str . slice ( 0 , size ));
buffer . set ( bytes );
return buffer ;
};
const hypothesis = encodeString (
"Caffeine improves long-term memory consolidation" ,
128
);
Decoding Example
// Read from on-chain account
const decodeString = ( bytes : Uint8Array ) : string => {
const nullIndex = bytes . indexOf ( 0 );
const trimmed = nullIndex === - 1 ? bytes : bytes . slice ( 0 , nullIndex );
return new TextDecoder (). decode ( trimmed );
};
const hypothesis = decodeString ( nullResult . hypothesis );
// "Caffeine improves long-term memory consolidation"
Statistical Fields
P-Value (Fixed-Point)
Stored as a 4-byte unsigned integer using fixed-point representation:
p_value = 8700 → 0.8700
p_value = 5000 → 0.5000
p_value = 123 → 0.0123
Conversion:
const pValueToU32 = ( p : number ) : number => Math . floor ( p * 10000 );
const u32ToPValue = ( raw : number ) : number => raw / 10000 ;
// Example
pValueToU32 ( 0.8700 ) // 8700
u32ToPValue ( 8700 ) // 0.8700
Sample Size
Stored as a 4-byte unsigned integer (supports up to 4,294,967,295 samples):
pub sample_size : u32 , // Direct integer, no conversion needed
Status Field
NKAs have three possible states:
Value Status Description 0Pending Newly submitted, awaiting community review 1Verified Validated by community or DAO 2Disputed Flagged for review or concerns
The status field is designed for future decentralized verification networks . Currently all NKAs start in Pending state.
Submission Flow
Complete Guided Form
Four-step form collects hypothesis, methodology, outcomes, and data file
Client-Side Hashing
Browser computes SHA-256 hash of data file (never uploads raw data)
Transaction Build
Frontend encodes all fields into fixed-size byte arrays
Wallet Signature
User signs transaction in Phantom wallet
On-Chain Execution
Program creates PDA, increments counter, emits NullResultSubmitted event
Instant Discovery
NKA appears on Dashboard with specimen ID (e.g., NKA-0042)
Code Example: Submit NKA
import { useAnchorWallet } from '@solana/wallet-adapter-react' ;
import { Program } from '@coral-xyz/anchor' ;
import { PublicKey } from '@solana/web3.js' ;
const submitNullResult = async (
program : Program ,
hypothesis : string ,
methodology : string ,
expectedOutcome : string ,
actualOutcome : string ,
pValue : number ,
sampleSize : number ,
dataHash : Uint8Array
) => {
const wallet = useAnchorWallet ();
if ( ! wallet ) throw new Error ( 'Wallet not connected' );
// Derive PDAs
const [ protocolState ] = PublicKey . findProgramAddressSync (
[ Buffer . from ( 'protocol_state' )],
program . programId
);
const state = await program . account . protocolState . fetch ( protocolState );
const nextSpecimenNumber = state . nkaCounter . toNumber () + 1 ;
const [ nullResult ] = PublicKey . findProgramAddressSync (
[
Buffer . from ( 'null_result' ),
wallet . publicKey . toBuffer (),
Buffer . from ( new BigUint64Array ([ BigInt ( nextSpecimenNumber )]). buffer ),
],
program . programId
);
// Encode fields
const encodeString = ( str : string , size : number ) => {
const buffer = new Uint8Array ( size );
buffer . set ( new TextEncoder (). encode ( str . slice ( 0 , size )));
return Array . from ( buffer );
};
// Submit transaction
const tx = await program . methods
. submitNullResult (
encodeString ( hypothesis , 128 ),
encodeString ( methodology , 128 ),
encodeString ( expectedOutcome , 128 ),
encodeString ( actualOutcome , 128 ),
Math . floor ( pValue * 10000 ), // Fixed-point conversion
sampleSize ,
Array . from ( dataHash )
)
. accounts ({
researcher: wallet . publicKey ,
protocolState ,
nullResult ,
})
. rpc ();
console . log ( `NKA created: NKA- ${ String ( nextSpecimenNumber ). padStart ( 4 , '0' ) } ` );
console . log ( `Transaction: ${ tx } ` );
};
Event Emission
Every NKA submission emits an on-chain event:
#[event]
pub struct NullResultSubmitted {
pub specimen_number : u64 ,
pub researcher : Pubkey ,
}
This enables real-time indexing and notification systems for new null results.
Storage Cost
Each NKA account requires rent-exempt storage:
Account discriminator: 8 bytes
NullResult data: 595 bytes (32+8+128+128+128+128+4+4+32+1+8+1)
────────────────────────────
Total: 603 bytes
Rent cost: ~0.0043 SOL per NKA (paid once by researcher)
Unlike traditional databases, this storage is permanent and censorship-resistant . Once created, an NKA exists forever on Solana.
Browse & Query
Fetch all NKAs from the program:
const nullResults = await program . account . nullResult . all ();
console . log ( `Total NKAs: ${ nullResults . length } ` );
nullResults . forEach (({ publicKey , account }) => {
const specimenId = `NKA- ${ String ( account . specimenNumber ). padStart ( 4 , '0' ) } ` ;
const hypothesis = decodeString ( account . hypothesis );
console . log ( ` ${ specimenId } : ${ hypothesis } ` );
});
Security Model
Ownership & Permissions
Only the researcher’s wallet can create NKAs under their identity
PDA seeds include researcher.key() to prevent impersonation
All accounts are program-owned PDAs (cannot be modified after creation)
Immutability ensures permanent record integrity
Next Steps
Bounty Marketplace Learn how to monetize your NKAs through bounties
Protocol Fees Understand the 2.5% fee model and treasury distribution