On-chain attestation — Neon Law Node (Solana)
Status: scaffolded, not shipped. The durable local record and the workflow seam are implemented and tested; the on-chain write itself is deferred behind a trait. The Node product page says so plainly, and the code cannot lie about it — see Why it can't claim a false record.
Neon Law Node is an attorney attestation recorded on-chain: a licensed attorney confirms a fact from the records a
client provides, and a hash of the signed attestation is written to Solana, binding the firm's wallet, the client's
wallet, and that hash. Solana is the chain because the workspace is Rust top-to-bottom — Solana programs are written in
Rust and Anchor is a framework of Rust macros — so the same workspace speaks to the
chain natively. The marketing copy lives in web/content/marketing/node.md; the binding engagement letter is
notation_templates/onboarding/retainer_node.md.
What is built
The vertical is implemented end-to-end except the chain write, mirroring how cloud::StorageService keeps GCS out of
the generic layer:
- The local record is the system of record. Every attestation writes one row to the
attestationstable (store::attestations, migrationm20260722), keyed unique onnotation_id. The row carries the document SHA-256 plus identifiers (wallets, PDA, transaction signature) and astatusofpending/recorded/failed— never client content, the same trust boundary telemetry observes. Postgres is snapshotted nightly to Parquet by thearchivesworkflow, so the attestation inherits the firm's ten-year retention for free. - The chain is isolated behind a trait.
workflows::attest::Attestorhas one method,record, returning aRecordedTxorNone.NullAttestoris the default (records nothing);attestor_from_envreadsNAVIGATOR_ONCHAIN_BACKEND(nulldefault,solanareserved and currently erroring by design). Selecting a chain — or a second chain later — is a newimpl Attestor, never a workflow edit. - The step is provider-neutral.
StepKind::OnChainRecordbinds theonchain__prefix;dispatch_onchain_recordhashes the document at the payload'sstorage_key, calls the attestor, and writes the row inside the worker'sctx.run(replay-idempotent). The attestor rides onStepDepsviawith_attestor, so a step reached without one errors clearly rather than silently skipping.
Why it can't claim a false record
The honesty is enforced by code, not copy. dispatch_onchain_record sets status = recorded only when the attestor
returns a real RecordedTx; NullAttestor returns None, so the row stays pending with no transaction. And
attestor_from_env makes the solana backend error at startup until the real implementation lands — a deployer can
never silently believe attestations are going on-chain when they are not. This is why the step is deliberately not yet
wired into the binding onboarding__retainer_node workflow: a binding retainer must not route through a step that, in
its current default, records nothing.
What is deferred
Two pieces of code and four decisions. The code is straightforward; the decisions are the real gate and want a council before any keypair touches production.
The code
1. SolanaAttestor — an impl Attestor holding an RPC client, the program id, and the firm signer. It builds the
record_attestation instruction, submits it, waits for the chosen commitment, and returns the signature + PDA.
2. The Anchor program — one instruction, record_attestation, that initializes a Program Derived Address seeded by
the notation id (init, seeds = [b"attestation", notation_id], bump, payer = firm), storing:
struct Attestation { firm: Pubkey, client: Pubkey, sha256: [u8; 32], recorded_at: i64 }
The PDA is also the exactly-once key: a replayed submit hits "account already in use", which the attestor treats as
success — the chain itself dedupes, complementing the journaled ctx.run.
3. The workflow edge — the one-line YAML change in retainer_node.md, routing the signature into the new step:
sent_for_signature__pending:
signature_received: onchain__record_attestation
signature_declined: END
onchain__record_attestation:
attestation_recorded: END
attestation_failed: staff_review
This ripples into the retainer / e-signature test suite (the tests that assert signature_received → END), so it lands
with the SolanaAttestor and its test updates, not before.
The decisions (not code)
"It's written in Rust" chose the SDK; it did not answer any of these. Each gates production:
- Firm key custody. The
SolanaAttestorsigns and pays fees from a firm wallet. That keypair belongs in KMS / Secret Manager with rotation — neverSOLANA_SIGNER_SECRETas a path on disk..env.exampleholds a reference, not the key. - Client wallet. The retainer promises "the client's wallet." Do we collect a client public key at intake (a new questionnaire field — none exists today) or mint a custodial one? This is a product decision with a UX cost.
- Public-chain confidentiality. Only the hash + two public keys + a timestamp go on-chain, never content — but a hash and two wallets are world-readable forever. The retainer's RPC 1.6 clause already discloses this; confirm the client's informed consent covers a permanent public record.
- Finality and cost. Transition to
attestation_recordedonly atfinalizedcommitment. Fees are SOL, passed through "at cost" per the retainer, so the firm needs a funded treasury and a devnet → mainnet switch.
Configuration
NAVIGATOR_ONCHAIN_BACKEND selects the backend (null default). When the SolanaAttestor ships it reads
SOLANA_RPC_URL, SOLANA_PROGRAM_ID, and a KMS reference for the signer. See .env.example for the committed
contract.
Pointers
- Seam and dispatch:
workflows::attest(theAttestortrait,NullAttestor,dispatch_onchain_record). - Local record:
store::attestations+ theattestationsentity / migrationm20260722. - Step kind / status table:
workflows::stepanddocs/notation-authoring.md(theonchain__row). - Product surfaces:
web/content/marketing/node.md,notation_templates/onboarding/retainer_node.md.