Skip to content

Commit

Permalink
fix(core): Loading at an AnchorCommit should be able to inform node t…
Browse files Browse the repository at this point in the history
…hat CACAO is not actually expired
  • Loading branch information
stbrody committed Jul 20, 2023
1 parent 8aaec9b commit 9a6b415
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 6 deletions.
5 changes: 5 additions & 0 deletions packages/common/src/streamopts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ export interface InternalOpts {
* @private
*/
throwIfStale?: boolean

/**
* If true, will not validate that CACAOs used to authorize commit signatures have not expired.
*/
skipCacaoExpirationChecks?: boolean
}

/**
Expand Down
21 changes: 17 additions & 4 deletions packages/core/src/state-management/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Context,
CreateOpts,
DiagnosticsLogger,
InternalOpts,
LoadOpts,
PinningOpts,
PublishOpts,
Expand Down Expand Up @@ -215,7 +216,7 @@ export class Repository {
* Starts by checking if the stream state is present in the in-memory cache, if not then
* checks the state store, and finally loads the stream from pubsub.
*/
async load(streamId: StreamID, opts: LoadOpts): Promise<RunningState> {
async load(streamId: StreamID, opts: LoadOpts & InternalOpts): Promise<RunningState> {
opts = { ...DEFAULT_LOAD_OPTS, ...opts }

const [state$, synced] = await this.loadingQ.forStream(streamId).run(async () => {
Expand Down Expand Up @@ -259,7 +260,10 @@ export class Repository {
}
})

StreamUtils.checkForCacaoExpiration(state$.state)
if (!opts.skipCacaoExpirationChecks) {
StreamUtils.checkForCacaoExpiration(state$.state)
}

if (synced && state$.isPinned) {
this.stateManager.markPinnedAndSynced(state$.id)
}
Expand All @@ -277,8 +281,17 @@ export class Repository {
// for the stream than is ultimately necessary, but doing so increases the chances that we
// detect that the CommitID specified is rejected by the conflict resolution rules due to
// conflict with the stream's canonical branch of history.
const base$ = await this.load(commitId.baseID, opts)
return this.stateManager.atCommit(base$, commitId)
// We also skip CACAO expiration checking during this initial load as its possible
// that the CommitID we are being asked to load may in fact be an anchor commit with
// the timestamp information that will reveal to us that the CACAO didn't actually expire.
const optsSkippingCACAOChecks = { ...opts, skipCacaoExpirationChecks: true }
const base$ = await this.load(commitId.baseID, optsSkippingCACAOChecks)
const stateAtCommit = await this.stateManager.atCommit(base$, commitId)

// Since we skipped CACAO expiration checking earlier we need to make sure to do it here.
StreamUtils.checkForCacaoExpiration(stateAtCommit.state)

return stateAtCommit
}

/**
Expand Down
58 changes: 56 additions & 2 deletions packages/stream-tests/src/__tests__/capability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ import {
ModelInstanceDocument,
ModelInstanceDocumentMetadata,
} from '@ceramicnetwork/stream-model-instance'
import { StreamID } from '@ceramicnetwork/streamid'
import { CommitID, StreamID } from '@ceramicnetwork/streamid'
import { Model, ModelDefinition } from '@ceramicnetwork/stream-model'
import { InMemoryAnchorService } from '@ceramicnetwork/core/lib/anchor/memory/in-memory-anchor-service.js'
import { jest } from '@jest/globals'
import type { CID } from 'multiformats/cid'

function getModelDef(name: string): ModelDefinition {
return {
Expand Down Expand Up @@ -503,6 +506,7 @@ describe('CACAO Integration test', () => {

afterEach(() => {
MockDate.reset()
jest.resetAllMocks()
})

test(
Expand Down Expand Up @@ -572,7 +576,7 @@ describe('CACAO Integration test', () => {
expect(loaded4.state.log).toEqual(loaded3.state.log) // Rewritten!
}, 30000)

test('overwrite expired capability when using RESYNC_ON_ERROR', async () => {
test('overwrite expired capability when using SYNC_ON_ERROR', async () => {
const opts = { asDID: didKeyWithCapability, anchor: false, publish: false }
const tile = await TileDocument.deterministic(
ceramic,
Expand Down Expand Up @@ -645,6 +649,56 @@ describe('CACAO Integration test', () => {
1000 * 30
)

test('Load at anchor CommitID to inform node of anchor meaning CACAO isnt actually expired', async () => {
const opts = { asDID: didKeyWithCapability, anchor: false, publish: false }
const tile = await TileDocument.create(
ceramic,
CONTENT0,
{
controllers: [`did:pkh:eip155:1:${wallet.address}`],
},
opts
)
await tile.update(CONTENT1, null, { ...opts, anchor: true })

// Anchor the update but ensure the Ceramic node doesn't learn about the anchor commit
const stateManager = ceramic.repository.stateManager
const handleAnchorSpy = jest.spyOn(stateManager, '_handleAnchorCommit')
const anchorCommitPromise = new Promise<CID>((resolve) => {
handleAnchorSpy.mockImplementation((state, tip, anchorCommit: CID, witnessCar) => {
expect(tip).toEqual(tile.tip)
resolve(anchorCommit)
})
})

const anchorService = ceramic.context.anchorService as InMemoryAnchorService
await anchorService.anchor()

const anchorCommitCID = await anchorCommitPromise

// Expire the CACAO, loading should fail
expireCacao()
await expect(TileDocument.load(ceramic, tile.id)).rejects.toThrow(/CACAO expired/) // No sync options

// Loading at the anchor commits CommitID should succeed
const commitIDAtAnchor = CommitID.make(tile.id, anchorCommitCID)
const loadedAtCommit = await TileDocument.load(ceramic, commitIDAtAnchor)
expect(loadedAtCommit.state.log.length).toEqual(3)
expect(loadedAtCommit.state.anchorStatus).toEqual(AnchorStatus.ANCHORED)

// Now loading the stream should work because the node now knows about the anchor
const loaded = await TileDocument.load(ceramic, tile.id)
expect(loaded.state.log.length).toEqual(3)
expect(loaded.state.anchorStatus).toEqual(AnchorStatus.ANCHORED)

// Resyncing outdated handle with the server should pick up the anchor commit
expect(tile.state.log.length).toEqual(2)
expect(tile.state.anchorStatus).not.toEqual(AnchorStatus.ANCHORED)
await tile.sync()
expect(tile.state.log.length).toEqual(3)
expect(tile.state.anchorStatus).toEqual(AnchorStatus.ANCHORED)
}, 30000)

test(
'Genesis commit applied with valid capability that later expires without being anchored',
async () => {
Expand Down

0 comments on commit 9a6b415

Please sign in to comment.