@atproto/repo
Version:
atproto repo and MST implementation
204 lines (194 loc) • 5.78 kB
text/typescript
import { CID } from 'multiformats/cid'
import { BlockMap } from '../block-map'
import { readCarWithRoot } from '../car'
import { DataDiff } from '../data-diff'
import { MST } from '../mst'
import { ReadableRepo } from '../readable-repo'
import { MemoryBlockstore, ReadableBlockstore, SyncStorage } from '../storage'
import {
RecordCidClaim,
RecordClaim,
VerifiedDiff,
VerifiedRepo,
def,
} from '../types'
import * as util from '../util'
export const verifyRepoCar = async (
carBytes: Uint8Array,
did?: string,
signingKey?: string,
): Promise<VerifiedRepo> => {
const car = await readCarWithRoot(carBytes)
return verifyRepo(car.blocks, car.root, did, signingKey)
}
export const verifyRepo = async (
blocks: BlockMap,
head: CID,
did?: string,
signingKey?: string,
opts?: { ensureLeaves?: boolean },
): Promise<VerifiedRepo> => {
const diff = await verifyDiff(null, blocks, head, did, signingKey, opts)
const creates = util.ensureCreates(diff.writes)
return {
creates,
commit: diff.commit,
}
}
export const verifyDiffCar = async (
repo: ReadableRepo | null,
carBytes: Uint8Array,
did?: string,
signingKey?: string,
opts?: { ensureLeaves?: boolean },
): Promise<VerifiedDiff> => {
const car = await readCarWithRoot(carBytes)
return verifyDiff(repo, car.blocks, car.root, did, signingKey, opts)
}
export const verifyDiff = async (
repo: ReadableRepo | null,
updateBlocks: BlockMap,
updateRoot: CID,
did?: string,
signingKey?: string,
opts?: { ensureLeaves?: boolean },
): Promise<VerifiedDiff> => {
const { ensureLeaves = true } = opts ?? {}
const stagedStorage = new MemoryBlockstore(updateBlocks)
const updateStorage = repo
? new SyncStorage(stagedStorage, repo.storage)
: stagedStorage
const updated = await verifyRepoRoot(
updateStorage,
updateRoot,
did,
signingKey,
)
const diff = await DataDiff.of(updated.data, repo?.data ?? null)
const writes = await util.diffToWriteDescripts(diff)
const newBlocks = diff.newMstBlocks
const leaves = updateBlocks.getMany(diff.newLeafCids.toList())
if (leaves.missing.length > 0 && ensureLeaves) {
throw new Error(`missing leaf blocks: ${leaves.missing}`)
}
newBlocks.addMap(leaves.blocks)
const removedCids = diff.removedCids
const commitCid = await newBlocks.add(updated.commit)
// ensure the commit cid actually changed
if (repo) {
if (commitCid.equals(repo.cid)) {
newBlocks.delete(commitCid)
} else {
removedCids.add(repo.cid)
}
}
return {
writes,
commit: {
cid: updated.cid,
rev: updated.commit.rev,
prev: repo?.cid ?? null,
since: repo?.commit.rev ?? null,
newBlocks,
relevantBlocks: newBlocks,
removedCids,
},
}
}
// @NOTE only verifies the root, not the repo contents
const verifyRepoRoot = async (
storage: ReadableBlockstore,
head: CID,
did?: string,
signingKey?: string,
): Promise<ReadableRepo> => {
const repo = await ReadableRepo.load(storage, head)
if (did !== undefined && repo.did !== did) {
throw new RepoVerificationError(`Invalid repo did: ${repo.did}`)
}
if (signingKey !== undefined) {
const validSig = await util.verifyCommitSig(repo.commit, signingKey)
if (!validSig) {
throw new RepoVerificationError(
`Invalid signature on commit: ${repo.cid.toString()}`,
)
}
}
return repo
}
export const verifyProofs = async (
proofs: Uint8Array,
claims: RecordCidClaim[],
did: string,
didKey: string,
): Promise<{ verified: RecordCidClaim[]; unverified: RecordCidClaim[] }> => {
const car = await readCarWithRoot(proofs)
const blockstore = new MemoryBlockstore(car.blocks)
const commit = await blockstore.readObj(car.root, def.commit)
if (commit.did !== did) {
throw new RepoVerificationError(`Invalid repo did: ${commit.did}`)
}
const validSig = await util.verifyCommitSig(commit, didKey)
if (!validSig) {
throw new RepoVerificationError(
`Invalid signature on commit: ${car.root.toString()}`,
)
}
const mst = MST.load(blockstore, commit.data)
const verified: RecordCidClaim[] = []
const unverified: RecordCidClaim[] = []
for (const claim of claims) {
const found = await mst.get(
util.formatDataKey(claim.collection, claim.rkey),
)
const record = found ? await blockstore.readObj(found, def.map) : null
if (claim.cid === null) {
if (record === null) {
verified.push(claim)
} else {
unverified.push(claim)
}
} else {
if (claim.cid.equals(found)) {
verified.push(claim)
} else {
unverified.push(claim)
}
}
}
return { verified, unverified }
}
export const verifyRecords = async (
proofs: Uint8Array,
did: string,
signingKey: string,
): Promise<RecordClaim[]> => {
const car = await readCarWithRoot(proofs)
const blockstore = new MemoryBlockstore(car.blocks)
const commit = await blockstore.readObj(car.root, def.commit)
if (commit.did !== did) {
throw new RepoVerificationError(`Invalid repo did: ${commit.did}`)
}
const validSig = await util.verifyCommitSig(commit, signingKey)
if (!validSig) {
throw new RepoVerificationError(
`Invalid signature on commit: ${car.root.toString()}`,
)
}
const mst = MST.load(blockstore, commit.data)
const records: RecordClaim[] = []
const leaves = await mst.reachableLeaves()
for (const leaf of leaves) {
const { collection, rkey } = util.parseDataKey(leaf.key)
const record = await blockstore.attemptReadRecord(leaf.value)
if (record) {
records.push({
collection,
rkey,
record,
})
}
}
return records
}
export class RepoVerificationError extends Error {}