@ar.io/anchor
v0.1.2
Published
Anchor-at-write-time for the ar.io verification stack. Hash your data locally, sign a Verifiable Event Envelope (ario.events/v1), and anchor it to ar.io — permanent, tamper-evident provenance with Merkle-batched checkpoints and per-event inclusion proofs.
Downloads
427
Maintainers
Readme
@ar.io/anchor
Anchor-at-write-time for the ar.io verification stack: hash your data locally, sign a Verifiable Event Envelope under the ario.events/v1 profile, and anchor it to ar.io — no relay, no SDK bloat, raw bytes never leave your system.
Write through Turbo — ar.io's upload service. Read it back through any ar.io gateway — turbo-gateway.com, or any of the others at gateways.ar.io; you're never locked to one provider. Underneath, it's stored permanently on Arweave.
npm install @ar.io/anchorimport { createAnchorer } from "@ar.io/anchor";
// Dev mode: zero config. Auto identity + wallet; small uploads are free on Turbo.
// Proofs are permanently marked environment:"dev" inside the signed bytes.
const ario = createAnchorer();
const receipt = await ario.anchor({
type: "event",
data: fileBytes, // or a string, or an AsyncIterable (streams),
// contentHash: "...", // or a pre-computed sha256 — exactly one
ref: "s3://bucket/key", // optional locator
metadata: { approver: "alice" },
chain: "orders", // optional per-key hash chain
});
receipt.txId; // ar.io transaction id (resolved once Turbo accepts the upload)
receipt.envelope; // the signed, Minimal-disclosure envelope
receipt.recordBytes; // RETAIN THESE — the committed event record
receipt.gatewayUrl; // resolve on any ar.io gateway (gateways.ar.io)Batching
High-frequency events (LLM steps, pipeline records): one ar.io write per window, while every event keeps its own offline-verifiable inclusion proof.
const batch = ario.batch({
maxEvents: 100, // flush when full…
maxAge: 60_000, // …or 60s after the first buffered event…
flushOnIdle: 5_000, // …or 5s after the last add. First trigger wins.
});
const receipt = await batch.add({ data: JSON.stringify(step) }).receipt();
// → { checkpointTxId, root, leafHash, leafIndex, auditPath, envelope, recordBytes, ... }
await ario.close(); // explicit flush — call it on shutdown; nothing is flushed for youadd() is synchronous (bytes/string/pre-computed hash — no streams here). A batch of one is valid. With the default in-memory buffer a crash loses buffered proofs, never data — every event is re-anchorable from your system.
Verifying
Fetch the envelope by txId from any ar.io gateway — https://<gateway>/raw/<txId> (e.g. turbo-gateway.com; browse the network at gateways.ar.io). With that envelope and your retained recordBytes, anyone verifies offline with the read-only @ar.io/proof:
import { ed25519Verify, jcs, sha256Hex, utf8 } from "@ar.io/proof";
const { signature, ...preSignature } = receipt.envelope;
await ed25519Verify(signature, utf8(jcs(preSignature)), receipt.envelope.public_key); // true
(await sha256Hex(receipt.recordBytes)) === receipt.envelope.payload_hash; // trueBatched events verify their inclusion the same way — see the runnable examples.
Production
Production structurally refuses auto-generated secrets — it throws unless you pass all three:
import { createAnchorer, LocalEd25519Signer, SolanaWalletSigner } from "@ar.io/anchor";
const ario = createAnchorer({
environment: "production",
// Identity key (signs envelopes). Any { publicKey(), sign() } works —
// file seed shown; Vault/KMS adapters implement the same interface.
signer: LocalEd25519Signer.fromSeedHex(process.env.ANCHOR_IDENTITY_SEED!),
// Funding wallet (pays Turbo) — a different key, Solana ed25519 default.
wallet: new SolanaWalletSigner(LocalEd25519Signer.fromSeedHex(process.env.ANCHOR_WALLET_SEED!)),
subject: { type: "producer", producer_id: "acme-app" },
});The two keys are deliberately separate: identity (who signed) vs money (who pays). Fund the wallet with Turbo Credits at https://turbo.ardrive.io.
Errors
All errors carry a machine-checkable code:
| Class (code) | When | Do |
|---|---|---|
| FundingExhaustedError (FUNDING_EXHAUSTED) | Turbo 402 — wallet out of funds | Fund the wallet; the message includes instructions. Not retryable as-is. |
| UploadFailedError (UPLOAD_FAILED) | 5xx/429/network, retries exhausted | Transient — safe to retry the same anchor() call. |
| UploadRejectedError (UPLOAD_REJECTED) | Terminal 4xx from Turbo | Inspect the detail; retrying the same bytes won't help. |
| TxIdMismatchError (TXID_MISMATCH) | Upstream returned a TX ID that doesn't match the signature-derived one | Should never happen with an honest upstream — treat the upload as suspect. |
| ProductionConfigError (PRODUCTION_CONFIG) | Production mode without explicit signer/wallet/subject | Supply the missing credentials; dev secrets cannot reach production. |
A failed batch window rejects only that window's receipt() promises — the chain head is untouched and the next window proceeds; re-add the events to re-anchor them.
Guarantees
- Local hashing only.
datais hashed in-process (streams supported); only the signed envelope (a few hundred bytes) is uploaded. - Minimal disclosure. The on-chain envelope carries no event type, no subject, no chain pointer — those live in the hash-committed record you retain.
- Dev proofs are cryptographically dev.
environmentsits inside the signed scope; a dev proof can never be presented as production evidence. - Verification is a separate, read-only package —
@ar.io/proof. This package contains the write path only.
Conformance
Byte-for-byte against the family corpus (ar-io-proof test-vectors-v1.1); ANS-104 output byte-pinned vs arbundles and re-verified by an independent Python parser in CI. See the profile spec.
