UNPKG

@atproto/repo

Version:

atproto repo and MST implementation

234 lines (212 loc) 6.4 kB
import { CID } from 'multiformats/cid' import { TID, dataToCborBlock } from '@atproto/common' import * as crypto from '@atproto/crypto' import { lexToIpld } from '@atproto/lexicon' import { BlockMap } from './block-map' import { CidSet } from './cid-set' import { DataDiff } from './data-diff' import log from './logger' import { MST } from './mst' import { ReadableRepo } from './readable-repo' import { RepoStorage } from './storage' import { Commit, CommitData, RecordCreateOp, RecordWriteOp, WriteOpAction, def, } from './types' import * as util from './util' type Params = { storage: RepoStorage data: MST commit: Commit cid: CID } export class Repo extends ReadableRepo { storage: RepoStorage constructor(params: Params) { super(params) this.storage = params.storage } static async formatInitCommit( storage: RepoStorage, did: string, keypair: crypto.Keypair, initialWrites: RecordCreateOp[] = [], revOverride?: string, ): Promise<CommitData> { const newBlocks = new BlockMap() let data = await MST.create(storage) for (const record of initialWrites) { const cid = await newBlocks.add(record.record) const dataKey = util.formatDataKey(record.collection, record.rkey) data = await data.add(dataKey, cid) } const dataCid = await data.getPointer() const diff = await DataDiff.of(data, null) newBlocks.addMap(diff.newMstBlocks) const rev = revOverride ?? TID.nextStr() const commit = await util.signCommit( { did, version: 3, rev, prev: null, // added for backwards compatibility with v2 data: dataCid, }, keypair, ) const commitCid = await newBlocks.add(commit) return { cid: commitCid, rev, since: null, prev: null, newBlocks, relevantBlocks: newBlocks, removedCids: diff.removedCids, } } static async createFromCommit( storage: RepoStorage, commit: CommitData, ): Promise<Repo> { await storage.applyCommit(commit) return Repo.load(storage, commit.cid) } static async create( storage: RepoStorage, did: string, keypair: crypto.Keypair, initialWrites: RecordCreateOp[] = [], ): Promise<Repo> { const commit = await Repo.formatInitCommit( storage, did, keypair, initialWrites, ) return Repo.createFromCommit(storage, commit) } static async load(storage: RepoStorage, cid?: CID) { const commitCid = cid || (await storage.getRoot()) if (!commitCid) { throw new Error('No cid provided and none in storage') } const commit = await storage.readObj(commitCid, def.versionedCommit) const data = await MST.load(storage, commit.data) log.info({ did: commit.did }, 'loaded repo for') return new Repo({ storage, data, commit: util.ensureV3Commit(commit), cid: commitCid, }) } async formatCommit( toWrite: RecordWriteOp | RecordWriteOp[], keypair: crypto.Keypair, ): Promise<CommitData> { const writes = Array.isArray(toWrite) ? toWrite : [toWrite] const leaves = new BlockMap() let data = this.data for (const write of writes) { if (write.action === WriteOpAction.Create) { const cid = await leaves.add(write.record) const dataKey = write.collection + '/' + write.rkey data = await data.add(dataKey, cid) } else if (write.action === WriteOpAction.Update) { const cid = await leaves.add(write.record) const dataKey = write.collection + '/' + write.rkey data = await data.update(dataKey, cid) } else if (write.action === WriteOpAction.Delete) { const dataKey = write.collection + '/' + write.rkey data = await data.delete(dataKey) } } const dataCid = await data.getPointer() const diff = await DataDiff.of(data, this.data) const newBlocks = diff.newMstBlocks const removedCids = diff.removedCids const proofs = await Promise.all( writes.map((op) => data.getCoveringProof(util.formatDataKey(op.collection, op.rkey)), ), ) const relevantBlocks = new BlockMap() for (const proof of proofs) relevantBlocks.addMap(proof) const addedLeaves = leaves.getMany(diff.newLeafCids.toList()) if (addedLeaves.missing.length > 0) { throw new Error(`Missing leaf blocks: ${addedLeaves.missing}`) } newBlocks.addMap(addedLeaves.blocks) relevantBlocks.addMap(addedLeaves.blocks) const rev = TID.nextStr(this.commit.rev) const commit = await util.signCommit( { did: this.did, version: 3, rev, prev: null, // added for backwards compatibility with v2 data: dataCid, }, keypair, ) const commitBlock = await dataToCborBlock(lexToIpld(commit)) if (!commitBlock.cid.equals(this.cid)) { newBlocks.set(commitBlock.cid, commitBlock.bytes) relevantBlocks.set(commitBlock.cid, commitBlock.bytes) removedCids.add(this.cid) } return { cid: commitBlock.cid, rev, since: this.commit.rev, prev: this.cid, newBlocks, relevantBlocks, removedCids, } } async applyCommit(commitData: CommitData): Promise<Repo> { await this.storage.applyCommit(commitData) return Repo.load(this.storage, commitData.cid) } async applyWrites( toWrite: RecordWriteOp | RecordWriteOp[], keypair: crypto.Keypair, ): Promise<Repo> { const commit = await this.formatCommit(toWrite, keypair) return this.applyCommit(commit) } async formatResignCommit(rev: string, keypair: crypto.Keypair) { const commit = await util.signCommit( { did: this.did, version: 3, rev, prev: null, // added for backwards compatibility with v2 data: this.commit.data, }, keypair, ) const newBlocks = new BlockMap() const commitCid = await newBlocks.add(commit) return { cid: commitCid, rev, since: null, prev: null, newBlocks, relevantBlocks: newBlocks, removedCids: new CidSet([this.cid]), } } async resignCommit(rev: string, keypair: crypto.Keypair) { const formatted = await this.formatResignCommit(rev, keypair) return this.applyCommit(formatted) } } export default Repo