Skip to main content

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

  1. Researcher uploads data file through the frontend (CSV, Excel, JSON, etc.)
  2. Client-side hashing via Web Crypto API computes SHA-256 digest
  3. Hash stored on-chain in the data_hash field (32 bytes)
  4. 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.

Metadata Structure

All text fields use fixed-size byte arrays with zero-padding:
FieldSizeFormat
hypothesis128 bytesUTF-8, zero-padded
methodology128 bytesUTF-8, zero-padded
expected_outcome128 bytesUTF-8, zero-padded
actual_outcome128 bytesUTF-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:
ValueStatusDescription
0PendingNewly submitted, awaiting community review
1VerifiedValidated by community or DAO
2DisputedFlagged for review or concerns
The status field is designed for future decentralized verification networks. Currently all NKAs start in Pending state.

Submission Flow

1

Complete Guided Form

Four-step form collects hypothesis, methodology, outcomes, and data file
2

Client-Side Hashing

Browser computes SHA-256 hash of data file (never uploads raw data)
3

Transaction Build

Frontend encodes all fields into fixed-size byte arrays
4

Wallet Signature

User signs transaction in Phantom wallet
5

On-Chain Execution

Program creates PDA, increments counter, emits NullResultSubmitted event
6

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

Build docs developers (and LLMs) love