A TypeScript library for creating cryptographically signed attestations with verifiable claims organized in a DAG structure.
SAVE enables organizations to create transparent, verifiable proof-of-reserves attestations. It supports:
- Three types of claims: Inline (predefined structure + proof), source-backed (pointers to object claims), and aggregated (computed from other claims)
- Object claims: Proven data containers that other claims can reference via JSON Pointers
- Multi-party attestations: Aggregate claims from diverse sources (on-chain balances, custodians, auditors, CEXs, etc.)
- Flexible proof system: Support for signatures, ZK proofs, TEE attestations, workflow proofs, and more
- Onchain asset references: Link claims to specific token balances on specific chains
📖 Read the detailed Claims, Aggregations, and Proofs guide →
npm install @save/coreRequires Node.js >= 18.0.0
import {
createNumericClaim,
NumericClaim,
ObjectClaim,
signClaim,
sum,
subtract,
AttestationBuilder,
generateKeyPair,
} from '@save/core';
// Generate keys (in production, use existing keys)
const issuerKeys = generateKeyPair();
const custodianKeys = generateKeyPair();
// 1. External party creates and signs a claim (inline)
const balanceClaim = createNumericClaim({
id: 'eth_balance',
value: 1_000_000,
unit: 'USDC',
asset: {
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
chainId: 1, // Ethereum mainnet
},
description: 'Holdings on Ethereum',
});
const balanceProof = signClaim(balanceClaim, custodianKeys.privateKey, {
signerIdentity: 'Custodian Inc',
});
const wrappedClaim = NumericClaim.inline({
claim: balanceClaim,
proof: balanceProof,
});
// 2. Create an object claim with proven data (many claims can reference this)
const cexObjectClaim = new ObjectClaim({
id: 'cex_snapshot',
data: {
accounts: [{ balance: { value: 500000, unit: 'USDC' } }]
},
description: 'CEX account snapshot',
proof: signClaim(
{ id: 'cex_snapshot', claimType: 'inline', dataType: 'object', data: { /* ... */ } },
custodianKeys.privateKey,
{ signerIdentity: 'CEX Custodian' }
)
});
// 3. Create a source-backed claim (pointer to object claim)
const cexClaim = NumericClaim.sourceBacked({
id: 'cex_balance',
dataPointer: 'cex_snapshot#/accounts/0/balance',
description: 'CEX holdings'
});
// 4. Create an aggregated claim (combines inline and source-backed claims)
const totalClaim = NumericClaim.aggregated({
id: 'total_reserves',
data: { value: 1500000, unit: 'USDC' },
aggregation: sum('eth_balance', 'cex_balance')
});
// 5. Build and sign the attestation
const attestation = new AttestationBuilder({
issuer: {
identity: '0x1234...5678',
name: 'Protocol X',
},
createdAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
})
.addClaim(cexObjectClaim)
.addClaim(wrappedClaim)
.addClaim(cexClaim)
.addClaim(totalClaim)
.sign(issuerKeys.privateKey);
// Export
console.log(attestation.toJSON());Claims are standardized assertions about verifiable facts. SAVE supports three types of claims:
- Inline: Predefined structure (e.g., numeric claims) with both data and proof
- Source-backed: Pointer to data in an object claim that has a proof
- Aggregated: Computed from other claims using aggregation functions (can combine inline and source-backed claims)
Numeric claims are currently supported (quantitative assertions with value + unit):
// Inline claim (predefined structure with proof)
const claim = createNumericClaim({
id: 'treasury_balance',
value: 500_000,
unit: 'USDC',
asset: {
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
chainId: 1,
},
description: 'Treasury holdings',
});
// Source-backed claim (pointer to object claim)
const sourceClaim = NumericClaim.sourceBacked({
id: 'cex_balance',
dataPointer: 'dataSourceId#/path/to/value'
});
// Aggregated claim (combines other claims)
const totalClaim = NumericClaim.aggregated({
id: 'total',
data: { value: 500000, unit: 'USDC' },
aggregation: sum('claim1', 'claim2')
});Object claims are proven data containers that other claims can reference:
// First, create and sign the object claim content
const snapshotContent = {
id: 'cex_snapshot_001',
claimType: 'inline' as const,
dataType: 'object' as const,
data: {
accounts: [
{ balance: { value: 500000, unit: 'USD' } }
]
}
};
const snapshotProof = signClaim(snapshotContent, custodianKey, {
signerIdentity: 'CEX Custodian',
});
// Then create the ObjectClaim with the proof
const cexSnapshot = new ObjectClaim({
id: 'cex_snapshot_001',
data: {
accounts: [
{ balance: { value: 500000, unit: 'USD' } }
]
},
description: 'CEX account snapshot',
proof: snapshotProof,
});Claims can then reference this data using JSON Pointers:
const claim = NumericClaim.sourceBacked({
id: 'balance',
dataPointer: 'cex_snapshot_001#/accounts/0/balance'
});External parties (custodians, auditors) sign claims to attest to their validity:
const proof = signClaim(claim, privateKey, {
signerIdentity: 'Custodian A',
publicKeySource: 'https://custodian.example/.well-known/gpor-keys.json',
});
// Proof structure:
// {
// proofType: 'signature',
// trustModel: 'reputational',
// mechanism: 'signature',
// algorithm: 'ECDSA_secp256k1',
// signerPublicKey: '0x...',
// signerIdentity: 'Custodian A',
// publicKeySource: 'https://...',
// signature: '0x...',
// metadata: { createdAt: '...' }
// }Derive values from other claims using aggregation functions:
// Sum multiple claims
const totalReserves = NumericClaim.aggregated({
id: 'total_reserves',
data: { value: 0, unit: 'USDC' }, // Will be computed from aggregation
description: 'Total reserves across all chains',
aggregation: sum('eth_balance', 'arb_balance'),
});
// Subtract claims
const netPosition = NumericClaim.aggregated({
id: 'net_position',
data: { value: 0, unit: 'USDC' }, // Will be computed from aggregation
description: 'Net reserves after liabilities',
aggregation: subtract('total_reserves', 'liabilities'),
});
// Nested aggregations
const netInline = NumericClaim.aggregated({
id: 'net_inline',
data: { value: 0, unit: 'USDC' }, // Will be computed from aggregation
aggregation: subtract(
sum('eth_balance', 'arb_balance'),
'liabilities'
),
});Attestations bundle multiple claims into a signed document:
const attestation = new AttestationBuilder({
issuer: {
identity: '0x1234...5678',
name: 'Protocol X',
},
createdAt: new Date().toISOString(),
publicKeySource: 'https://protocol.example/.well-known/gpor-keys.json',
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
})
.addClaim(claim1)
.addClaim(claim2)
.addClaim(compositeClaim)
.sign(privateKey);// Export to file
await attestation.exportToFile('attestation.json');
// Load from file
const loaded = await Attestation.fromFile('attestation.json');Verify attestations programmatically or via CLI:
import { verifyAttestation } from '@save/core';
const verification = await verifyAttestation(attestationData, {
verifier: {
name: 'Auditor Inc',
publicKey: verifierPublicKey,
identity: '0xVerifier...',
},
verifiedAt: new Date().toISOString(),
signingKey: verifierPrivateKey,
});
console.log(verification.summary);
// {
// totalClaims: 8,
// validClaims: 8,
// invalidClaims: 0,
// uncertainClaims: 0,
// overallStatus: 'Valid'
// }CLI Tool:
# Verify an attestation
npx save-verify attestation.json
# With options
npx save-verify attestation.json \
--output verification.json \
--verifier-name "Auditor Inc" \
--verbose
# Publish to IPFS and on-chain registry
npx save-verify attestation.json \
--publish \
--env-file .env \
--registry 0x... \
--rpc-url https://...Numeric Claims:
createNumericClaim(options)- Create a numeric claim contentNumericClaim.inline({ claim, proof })- Create an inline claim from signed contentNumericClaim.sourceBacked({ id, dataPointer, unit?, ... })- Create a source-backed claimNumericClaim.aggregated({ id, data, aggregation, ... })- Create an aggregated claim
String Claims:
createStringClaim(options)- Create a string claim contentStringClaim.inline({ claim, proof })- Create an inline string claimStringClaim.sourceBacked({ id, dataPointer, expectedValue?, ... })- Create a source-backed string claim
Object Claims:
new ObjectClaim({ id, data, proof, ... })- Create an object claim (JSON format)new ObjectClaim({ id, format: 'structured-text', data, proof, ... })- Create a structured text claim
Utilities:
extractFromStructuredText(text, pointer)- Extract data from plain text using pointersresolveObjectPointer(data, pointer)- Resolve JSON pointers with transformations
signClaim(claim, privateKey, options?)- Sign a claim with ECDSAcreateSignatureProof(claim, privateKey, options?)- Create a signature proofgetClaimSigningData(claim)- Get canonical signing data for a claimcreateVlayerProof(claimContent, vlayerProofData, options?)- Create a ZK-TLS Notary proofverifyZkTlsNotaryProof(proof, options?)- Verify a ZK-TLS Notary proof
sum(...operands)- Sum multiple claims or nested aggregationssubtract(minuend, subtrahend)- Subtract one claim from anotherexecuteNumericAggregation(aggregation, resolver)- Execute an aggregation with custom resolver
Builder:
new AttestationBuilder(options)- Create a builder.addClaim(claim)- Add a claim (chainable).addClaims(claims)- Add multiple claims (chainable).sign(privateKey)- Sign and finalize the attestation
Attestation Class:
attestation.toJSON()- Export to JSON stringattestation.exportToFile(path)- Export to fileAttestation.fromJSON(json)- Load from JSON stringAttestation.fromFile(path)- Load from file
verifyAttestation(attestation, options)- Verify an attestation and generate verification documentregisterProofVerifier(proofType, verifierFn)- Register custom proof verifiers
new ClaimDAG()- Create a new claim dependency graph.addClaim(claim)- Add a claim to the DAG (resolves dependencies automatically).getClaim(id)- Get a claim by ID
generateKeyPair()- Generate a new secp256k1 key pairgetPublicKey(privateKey)- Derive public key from private keysign(data, privateKey)- Sign dataverify(signature, data, publicKey)- Verify a signaturehashData(data)- Hash data using SHA-256hashObject(obj)- Hash a JSON object deterministicallydeterministicId(seed)- Generate a deterministic UUID from a seed string
SAVE separates proof metadata into two orthogonal dimensions:
- Trust model (
trustModel): what you ultimately trust for correctness. This is informative only and open to interpretation. - Mechanism (
mechanism): how the claim is evidenced or verified. This must match the provided proof structure.
| Trust model | What you trust |
|---|---|
| reputational | A specific identity's assertion (e.g. signature) |
| mathematical | Mathematical verification (e.g. Merkle Proof) |
| computation | An execution environment or process (TEE or other externally sourced reproducible computations) |
Mechanisms are independent of trust models and describe how evidence is provided:
| Mechanism | Description | Status |
|---|---|---|
| signature | Cryptographic signatures — ECDSA secp256k1 supported; Ed25519 and BLS not yet implemented | Supported |
| multisig | Threshold signatures from multiple parties | Planned |
| api | Data retrieved from an API endpoint | Planned |
| document | Certified or signed documents | Planned |
| zk_tls_notary | ZK-TLS Notary proofs (cryptographic proof of web data via Vlayer) | Vlayer Supported |
| merkle_inclusion | Merkle tree inclusion proofs | Planned |
| state_proof | Blockchain state/storage proofs | Planned |
| range_proof | Range proofs for confidential values | Planned |
| tee | Trusted Execution Environment attestations (SGX, Nitro, SEV) | Planned |
| cre_consensus | Chainlink CRE DON consensus workflows | Supported as runtime |
| on_chain | Smart contract execution with blockchain consensus | Planned |
Trust models and mechanisms are composed together in proofs. Not all combination of trust model and mechanisms make sense. For example, an ECDSA signature mechanism with reputational trust means trusting the signer's identity, while a zk_proof mechanism with mathematical trust means trusting the math. But a computation trust model with an onchain reference mechanism makes little sense. However whether or not this makes sense is not enforced by the framework.
APACHE 2.0