DocsReference

Canonical hashing

All hashes are SHA-256 (a Solana syscall and an SP1 precompile), domain-separated.

01

The spec

This is BLINDSEAL.md §3.3 verbatim; three implementations reproduce it byte for byte and are tested against each other: the Rust detector core (host and in-zkVM), the on-chain program, and the TypeScript SDK.

text
payloadHash     = sha256(payload_bytes)            -- payload_bytes = canonical_json({tool, args}); no domain prefix in v1 (the shipped guest; see DECISIONS 2026-10-23)
policyRoot      = merkle_sha256(leaves), leaf_i = sha256("blindseal.rule.v1" || rule_id || layer || kind || compiled_bytes)
                  (compiled_bytes = DFA bytes for regex rules, raw value otherwise; tree sorted by rule_id; empty tree = sha256("blindseal.policy.empty.v1"))
detectorVersion = sp1 vkey hash of the guest program (bytes32)
actionHash      = sha256("blindseal.action.v1"
                  || chainId(u16 LE)           -- 1 = solana mainnet, 2 = reserved (no devnet), 3+ reserved for EVM
                  || targetProgram(32)
                  || sourceAuthority(32)       -- signer/authority the action executes under; zero if n/a
                  || instruction(8)            -- anchor discriminator or sha256(name)[..8]
                  || recipient(32)             -- zero if n/a
                  || mint(32)                  -- zero for native
                  || amount(u64 LE)            -- zero if n/a
                  || payloadHash(32)
                  || policyRoot(32)
                  || detectorVersion(32)
                  || expirySlot(u64 LE)
                  || nonce(32))                -- random per attestation
nullifier       = sha256("blindseal.nullifier.v1" || actionHash)

Nonce exists so two legitimately identical actions get distinct attestations. Nullifier is a pure function of the action hash so the consumer can’t be tricked into accepting a nullifier for a different action.

02

Public inputs

The proof commits one 168-byte blob in this order: payloadHash ‖ policyRoot ‖ detectorVersion ‖ actionHash ‖ nullifier (five × 32 bytes) then expirySlot as a little-endian u64. The verifier (on chain and in the browser) checks the proof against the blob; UIs decode the blob into the six named fields for display.

03

On-chain addresses

text
Config     = PDA(["config"])
PolicyRoot = PDA(["policy", policyRoot32])
Nullifier  = PDA(["nullifier", nullifier32])     -- exists ⇔ consumed
program    = merZNYKpKwwX4CGgzvma3jPvLfrMTkZcb1cSXUKANcb  (Solana mainnet)