Skip to content

Commit

Permalink
feat: support attach inline blocks in invocation and delegation (#288)
Browse files Browse the repository at this point in the history
  • Loading branch information
vasco-santos authored Apr 27, 2023
1 parent 04fcafe commit c9d6f3e
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 7 deletions.
2 changes: 1 addition & 1 deletion packages/core/src/dag.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const iterate = function* (value) {
/**
* @template [T=unknown]
* @param {API.Block<T>[]} blocks
* @returns {BlockStore<T>}
* @returns {API.BlockStore<T>}
*/
export const createStore = (blocks = []) => {
const store = new Map()
Expand Down
60 changes: 55 additions & 5 deletions packages/core/src/delegation.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ export class Delegation {
this.root = root
this.blocks = blocks

/** @type {API.AttachedLinkSet} */
this.attachedLinks = new Set()

Object.defineProperties(this, {
blocks: {
enumerable: false,
Expand Down Expand Up @@ -192,12 +195,25 @@ export class Delegation {
Object.defineProperties(this, { data: { value: data, enumerable: false } })
return data
}
/**
* Attach a block to the delegation DAG so it would be included in the
* block iterator.
* ⚠️ You should only attach blocks that are referenced from the `capabilities`
* or `facts`, if that is not the case you probably should reconsider.
* ⚠️ Once a delegation is de-serialized the attached blocks will not be re-attached.
*
* @param {API.Block} block
*/
attach(block) {
this.attachedLinks.add(`${block.cid}`)
this.blocks.set(`${block.cid}`, block)
}
export() {
return exportDAG(this.root, this.blocks)
return exportDAG(this.root, this.blocks, this.attachedLinks)
}

iterateIPLDBlocks() {
return exportDAG(this.root, this.blocks)
return exportDAG(this.root, this.blocks, this.attachedLinks)
}

/**
Expand Down Expand Up @@ -329,7 +345,7 @@ const decode = ({ bytes }) => {
*/

export const delegate = async (
{ issuer, audience, proofs = [], ...input },
{ issuer, audience, proofs = [], attachedBlocks = new Map, ...input },
options
) => {
const links = []
Expand Down Expand Up @@ -358,22 +374,56 @@ export const delegate = async (
const delegation = new Delegation({ cid, bytes }, blocks)
Object.defineProperties(delegation, { proofs: { value: proofs } })

for (const block of attachedBlocks.values()) {
delegation.attach(block)
}

return delegation
}

/**
* @template {API.Capabilities} C
* @param {API.UCANBlock<C>} root
* @param {DAG.BlockStore} blocks
* @param {API.AttachedLinkSet} attachedLinks
* @returns {IterableIterator<API.Block>}
*/

export const exportDAG = function* (root, blocks) {
export const exportDAG = function* (root, blocks, attachedLinks) {
for (const link of decode(root).proofs) {
// Check if block is included in this delegation
const root = /** @type {UCAN.Block} */ (blocks.get(`${link}`))
if (root) {
yield* exportSubDAG(root, blocks)
}
}

for (const link of attachedLinks.values()) {
const block = blocks.get(link)

/* c8 ignore next 3 */
if (!block) {
throw new Error(`Attached block with link ${link} is not in BlockStore`)
}
// @ts-expect-error can get blocks with v0 and v1
yield block
}

yield root
}

/**
* @template {API.Capabilities} C
* @param {API.UCANBlock<C>} root
* @param {DAG.BlockStore} blocks
* @returns {IterableIterator<API.Block>}
*/
const exportSubDAG = function* (root, blocks) {
for (const link of decode(root).proofs) {
// Check if block is included in this delegation
const root = /** @type {UCAN.Block} */ (blocks.get(`${link}`))
if (root) {
yield* exportDAG(root, blocks)
yield* exportSubDAG(root, blocks)
}
}

Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/invocation.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,22 @@ class IssuedInvocation {
this.notBefore = notBefore
this.nonce = nonce
this.facts = facts

/** @type {API.BlockStore<unknown>} */
this.attachedBlocks = new Map()
}

/**
* Attach a block to the invocation DAG so it would be included in the
* block iterator.
* ⚠️ You should only attach blocks that are referenced from the `capabilities`
* or `facts`, if that is not the case you probably should reconsider.
* ⚠️ Once a delegation is de-serialized the attached blocks will not be re-attached.
*
* @param {API.Block} block
*/
attach(block) {
this.attachedBlocks.set(`${block.cid}`, block)
}

delegate() {
Expand Down
25 changes: 25 additions & 0 deletions packages/core/test/delegation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { assert, test } from './test.js'
import { Delegation, UCAN, delegate, parseLink } from '../src/lib.js'
import { alice, bob, mallory, service as w3 } from './fixtures.js'
import { base64 } from 'multiformats/bases/base64'
import { getBlock } from './utils.js'

const utf8 = new TextEncoder()

const link = parseLink(
Expand Down Expand Up @@ -287,3 +289,26 @@ test('.buildIPLDView() return same value', async () => {

assert.equal(ucan.buildIPLDView(), ucan)
})

test('delegation.attach', async () => {
const ucan = await Delegation.delegate({
issuer: alice,
audience: bob,
capabilities: [
{
can: 'store/add',
with: alice.did(),
},
],
})

const block = await getBlock({ test: 'inlineBlock' })
ucan.attach(block)

const delegationBlocks = []
for (const b of ucan.iterateIPLDBlocks()) {
delegationBlocks.push(b)
}

assert.ok(delegationBlocks.find(b => b.cid.equals(block.cid)))
})
26 changes: 25 additions & 1 deletion packages/core/test/invocation.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { invoke, UCAN, Invocation } from '../src/lib.js'
import { alice, service as w3 } from './fixtures.js'
import { getBlock } from './utils.js'
import { assert, test } from './test.js'

test('encode invocation', async () => {
Expand Down Expand Up @@ -31,6 +32,30 @@ test('encode invocation', async () => {
assert.deepEqual(delegation.audience.did(), w3.did())
})

test('encode invocation with attached block', async () => {
const add = invoke({
issuer: alice,
audience: w3,
capability: {
can: 'store/add',
with: alice.did(),
link: 'bafy...stuff',
},
proofs: [],
})

const block = await getBlock({ test: 'inlineBlock' })
add.attach(block)

const delegationBlocks = []
const view = await add.buildIPLDView()
for (const b of view.iterateIPLDBlocks()) {
delegationBlocks.push(b)
}

assert.ok(delegationBlocks.find(b => b.cid.equals(block.cid)))
})

test('expired invocation', async () => {
const expiration = UCAN.now() - 5
const invocation = invoke({
Expand All @@ -40,7 +65,6 @@ test('expired invocation', async () => {
can: 'store/add',
with: alice.did(),
},

expiration,
})

Expand Down
14 changes: 14 additions & 0 deletions packages/core/test/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as Block from 'multiformats/block'
import * as codec from '@ipld/dag-cbor'
import { sha256 as hasher } from 'multiformats/hashes/sha2'

/**
* @param {any} value
*/
export async function getBlock(value) {
return await Block.encode({
value,
codec,
hasher
})
}
7 changes: 7 additions & 0 deletions packages/interface/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ export type {
}
export * as UCAN from '@ipld/dag-ucan'

export type BlockStore <T> = Map<ToString<Link>, Block<T, number, number, 1>>
export type AttachedLinkSet = Set<ToString<Link>>

/**
* Proof can either be a link to a delegated UCAN or a materialized {@link Delegation}
* view.
Expand All @@ -106,6 +109,7 @@ export interface UCANOptions {

facts?: Fact[]
proofs?: Proof[]
attachedBlocks?: BlockStore<unknown>
}

/**
Expand Down Expand Up @@ -241,6 +245,8 @@ export interface Delegation<C extends Capabilities = Capabilities>

toJSON(): DelegationJSON<this>
delegate(): Await<Delegation<C>>

attach(block: Block): void
}

/**
Expand Down Expand Up @@ -543,6 +549,7 @@ export interface IssuedInvocation<C extends Capability = Capability>
readonly proofs: Proof[]

delegate(): Await<Delegation<[C]>>
attach(block: Block): void
}

export type ServiceInvocation<
Expand Down

0 comments on commit c9d6f3e

Please sign in to comment.