Skip to content

Commit

Permalink
feat: router (#11)
Browse files Browse the repository at this point in the history
This PR adds a router interface:

```ts
/**
 * The routing service is responsible for selecting storage nodes to allocate
 * blobs with.
 */
export interface RoutingService {
  /**
   * Selects a candidate for blob allocation from the current list of available
   * storage nodes.
   */
  selectStorageProvider(
    digest: MultihashDigest,
    size: number
  ): Promise<Result<Principal, CandidateUnavailable | Failure>>
  /**
   * Returns information required to make an invocation to the requested storage
   * node.
   */
  configureInvocation<C extends BlobAllocate | BlobAccept>(
    provider: Principal,
    capability: C,
    options?: Omit<UCANOptions, 'audience'>
  ): Promise<Result<Configuration<C>, ProofUnavailable | Failure>>
}
```

Tests implement a storage provider node and setup 2 nodes that are used
in tests that the routing service picks randomly from. However the
router will be sticky when asked to store the same blob multiple times.
This allows existing tests for allocation and accept responses to pass.
Note: we need to review if the semantics for allocate and accept
responses really makes sense in a context where there are multiple
storage providers (TLDR; the allocation size does not).
  • Loading branch information
alanshaw authored Nov 5, 2024
2 parents ac49094 + ae0508c commit c810735
Show file tree
Hide file tree
Showing 92 changed files with 5,173 additions and 5,537 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/w3up-client.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
node-version: ${{ matrix.node_version }}
registry-url: https://registry.npmjs.org/
cache: 'pnpm'
- run: pnpm --filter '@storacha/client' install
- run: pnpm --filter '@storacha/client...' install
- run: pnpm --filter '@storacha/client' attw
- uses: ./packages/w3up-client/.github/actions/test
with:
Expand Down
6 changes: 3 additions & 3 deletions packages/access-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,15 @@
"@ipld/car": "^5.1.1",
"@ipld/dag-ucan": "^3.4.0",
"@scure/bip39": "^1.2.1",
"@storacha/capabilities": "workspace:^",
"@storacha/did-mailto": "workspace:^",
"@storacha/one-webcrypto": "^1.0.1",
"@ucanto/client": "^9.0.1",
"@ucanto/core": "^10.0.1",
"@ucanto/interface": "^10.0.1",
"@ucanto/principal": "^9.0.1",
"@ucanto/transport": "^9.1.1",
"@ucanto/validator": "^9.0.2",
"@storacha/capabilities": "workspace:^",
"@storacha/did-mailto": "workspace:^",
"bigint-mod-arith": "^3.1.2",
"conf": "11.0.2",
"multiformats": "^12.1.2",
Expand All @@ -118,6 +118,7 @@
"uint8arrays": "^4.0.6"
},
"devDependencies": {
"@storacha/eslint-config": "workspace:^",
"@types/assert": "^1.5.6",
"@types/inquirer": "^9.0.4",
"@types/mocha": "^10.0.1",
Expand All @@ -126,7 +127,6 @@
"@types/varint": "^6.0.1",
"@types/ws": "^8.5.4",
"@ucanto/server": "^10.0.0",
"@storacha/eslint-config": "workspace:^",
"assert": "^2.0.0",
"mocha": "^10.2.0",
"playwright-test": "^12.3.4",
Expand Down
2 changes: 1 addition & 1 deletion packages/access-client/src/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ export class Agent {
// @ts-ignore
capability: cap.create({
with: space,
nb: options.nb,
nb: 'nb' in options ? options.nb : undefined,
}),
issuer: this.issuer,
proofs: [...proofs],
Expand Down
2 changes: 1 addition & 1 deletion packages/access-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ export type InvokeOptions<
Match<{ can: A; with: R & Resource; nb: Caveats }, UnknownMatch>
>
> = UCANBasicOptions &
InferNb<InferInvokedCapability<CAP>['nb']> & {
Omit<InferInvokedCapability<CAP>, 'can' | 'with'> & {
/**
* Resource for the capability, normally a Space DID
* Defaults to the current selected Space
Expand Down
4 changes: 2 additions & 2 deletions packages/blob-index/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,17 @@
},
"dependencies": {
"@ipld/dag-cbor": "^9.0.6",
"@storacha/capabilities": "workspace:^",
"@storacha/one-webcrypto": "^1.0.1",
"@ucanto/core": "^10.0.1",
"@ucanto/interface": "^10.0.1",
"@storacha/capabilities": "workspace:^",
"carstream": "^2.1.0",
"multiformats": "^13.0.1",
"uint8arrays": "^5.0.3"
},
"devDependencies": {
"@ucanto/transport": "^9.1.1",
"@storacha/eslint-config": "workspace:^",
"@ucanto/transport": "^9.1.1",
"c8": "^7.14.0",
"entail": "^2.1.2",
"typescript": "5.2.2"
Expand Down
16 changes: 12 additions & 4 deletions packages/capabilities/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
"types": "./dist/test/helpers/*.d.ts",
"import": "./test/helpers/*.js"
},
"./blob": {
"types": "./dist/src/blob.d.ts",
"import": "./src/blob.js"
},
"./filecoin": {
"types": "./dist/src/filecoin/index.d.ts",
"import": "./src/filecoin/index.js"
Expand All @@ -57,9 +61,13 @@
"types": "./dist/src/filecoin/dealer.d.ts",
"import": "./src/filecoin/dealer.js"
},
"./index": {
"types": "./dist/src/index/index.d.ts",
"import": "./src/index/index.js"
"./space/index": {
"types": "./dist/src/space/index.d.ts",
"import": "./src/space/index.js"
},
"./space/blob": {
"types": "./dist/src/space/blob.d.ts",
"import": "./src/space/blob.js"
},
"./web3.storage/blob": {
"types": "./dist/src/web3.storage/blob.d.ts",
Expand Down Expand Up @@ -101,10 +109,10 @@
"uint8arrays": "^5.0.3"
},
"devDependencies": {
"@storacha/eslint-config": "workspace:^",
"@types/assert": "^1.5.6",
"@types/mocha": "^10.0.0",
"@types/node": "^20.8.4",
"@storacha/eslint-config": "workspace:^",
"assert": "^2.0.0",
"mocha": "^10.2.0",
"playwright-test": "^12.3.4",
Expand Down
2 changes: 1 addition & 1 deletion packages/capabilities/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import * as Filecoin from '@storacha/capabilities/filecoin'
import * as Aggregator from '@storacha/capabilities/filecoin/aggregator'
import * as DealTracker from '@storacha/capabilities/filecoin/deal-tracker'
import * as Dealer from '@storacha/capabilities/filecoin/dealer'
import * as Index from '@storacha/capabilities/index'
import * as Index from '@storacha/capabilities/space/index'

// This package has a "main" entrypoint but we recommend the usage of the specific imports above
```
Expand Down
200 changes: 56 additions & 144 deletions packages/capabilities/src/blob.js
Original file line number Diff line number Diff line change
@@ -1,177 +1,89 @@
/**
* Blob Capabilities.
*
* Blob is a fixed size byte array addressed by the multihash.
* Usually blobs are used to represent set of IPLD blocks at different byte ranges.
* The blob protocol allows authorized agents allocate memory space on a storage
* node and subsequently verify the content has been accepted by / delivered to
* said node.
*
* These can be imported directly with:
* ```js
* import * as Blob from '@storacha/capabilities/blob'
* import * as Index from '@storacha/capabilities/blob'
* ```
*
* @module
* @see https://github.com/storacha/specs/blob/main/w3-blob.md
*/
import { equals } from 'uint8arrays/equals'
import { capability, Schema, fail, ok } from '@ucanto/validator'
import { equalBlob, equalWith, SpaceDID } from './utils.js'

/**
* Agent capabilities for Blob protocol
*/
import { capability, Schema, Link, ok } from '@ucanto/validator'
import { content } from './space/blob.js'
import {
equalBlob,
equalWith,
SpaceDID,
and,
equal,
checkLink,
Await,
} from './utils.js'

/**
* Capability can only be delegated (but not invoked) allowing audience to
* derived any `space/blob/` prefixed capability for the (memory) space identified
* by DID in the `with` field.
* derive any `blob/` prefixed capability.
*/
export const blob = capability({
can: 'space/blob/*',
/**
* DID of the (memory) space where Blob is intended to
* be stored.
*/
with: SpaceDID,
can: 'blob/*',
/** Storage provider DID. */
with: Schema.did(),
derives: equalWith,
})

/**
* Blob description for being ingested by the service.
*/
export const content = Schema.struct({
/**
* A multihash digest of the blob payload bytes, uniquely identifying blob.
*/
digest: Schema.bytes(),
/**
* Number of bytes contained by this blob. Service will provision write target
* for this exact size. Attempt to write a larger Blob file will fail.
*/
size: Schema.integer(),
})

/**
* `space/blob/add` capability allows agent to store a Blob into a (memory) space
* identified by did:key in the `with` field. Agent should compute blob multihash
* and size and provide it under `nb.blob` field, allowing a service to provision
* a write location for the agent to PUT desired Blob into.
* The `blob/allocate` capability can be invoked to create a memory address on a
* storage node where blob content can be written via a HTTP PUT request.
*/
export const add = capability({
can: 'space/blob/add',
/**
* DID of the (memory) space where Blob is intended to
* be stored.
*/
with: SpaceDID,
export const allocate = capability({
can: 'blob/allocate',
/** Storage provider DID. */
with: Schema.did(),
nb: Schema.struct({
/**
* Blob to be added on the space.
*/
/** Blob to allocate. */
blob: content,
/** Link to the add blob task that initiated the allocation. */
cause: Schema.link({ version: 1 }),
/** DID of the user space where the allocation takes place. */
space: SpaceDID,
}),
derives: equalBlob,
})

/**
* Capability can be used to remove the stored Blob from the (memory)
* space identified by `with` field.
*/
export const remove = capability({
can: 'space/blob/remove',
/**
* DID of the (memory) space where Blob is stored.
*/
with: SpaceDID,
nb: Schema.struct({
/**
* A multihash digest of the blob payload bytes, uniquely identifying blob.
*/
digest: Schema.bytes(),
}),
derives: (claimed, delegated) => {
if (claimed.with !== delegated.with) {
return fail(
`Expected 'with: "${delegated.with}"' instead got '${claimed.with}'`
)
} else if (
delegated.nb.digest &&
!equals(delegated.nb.digest, claimed.nb.digest)
) {
return fail(
`Link ${
claimed.nb.digest ? `${claimed.nb.digest}` : ''
} violates imposed ${delegated.nb.digest} constraint.`
)
}
return ok({})
},
})

/**
* Capability can be invoked to request a list of stored Blobs in the
* (memory) space identified by `with` field.
*/
export const list = capability({
can: 'space/blob/list',
/**
* DID of the (memory) space where Blobs to be listed are stored.
*/
with: SpaceDID,
nb: Schema.struct({
/**
* A pointer that can be moved back and forth on the list.
* It can be used to paginate a list for instance.
*/
cursor: Schema.string().optional(),
/**
* Maximum number of items per page.
*/
size: Schema.integer().optional(),
}),
derives: (claimed, delegated) => {
if (claimed.with !== delegated.with) {
return fail(
`Expected 'with: "${delegated.with}"' instead got '${claimed.with}'`
)
}
return ok({})
},
derives: (claimed, delegated) =>
and(equalWith(claimed, delegated)) ||
and(equalBlob(claimed, delegated)) ||
and(checkLink(claimed.nb.cause, delegated.nb.cause, 'cause')) ||
and(equal(claimed.nb.space, delegated.nb.space, 'space')) ||
ok({}),
})

/**
* Capability can be used to get the stored Blob from the (memory)
* space identified by `with` field.
* The `blob/accept` capability invocation should either succeed when content is
* delivered on allocated address or fail if no content is allocation expires
* without content being delivered.
*/
export const get = capability({
can: 'space/blob/get/0/1',
/**
* DID of the (memory) space where Blob is stored.
*/
with: SpaceDID,
export const accept = capability({
can: 'blob/accept',
/** Storage provider DID. */
with: Schema.did(),
nb: Schema.struct({
/**
* A multihash digest of the blob payload bytes, uniquely identifying blob.
*/
digest: Schema.bytes(),
/** Blob to accept. */
blob: content,
/** DID of the user space where allocation took place. */
space: SpaceDID,
/** This task is blocked on `http/put` receipt available */
_put: Await,
}),
derives: (claimed, delegated) => {
if (claimed.with !== delegated.with) {
return fail(
`Expected 'with: "${delegated.with}"' instead got '${claimed.with}'`
)
} else if (
delegated.nb.digest &&
!equals(delegated.nb.digest, claimed.nb.digest)
) {
return fail(
`Link ${
claimed.nb.digest ? `${claimed.nb.digest}` : ''
} violates imposed ${delegated.nb.digest} constraint.`
)
}
return ok({})
},
derives: (claimed, delegated) =>
and(equalWith(claimed, delegated)) ||
and(equalBlob(claimed, delegated)) ||
and(equal(claimed.nb.space, delegated.nb.space, 'space')) ||
ok({}),
})

// ⚠️ We export imports here so they are not omitted in generated typedefs
// @see https://github.com/microsoft/TypeScript/issues/51548
export { Schema }
export { Schema, Link }
2 changes: 1 addition & 1 deletion packages/capabilities/src/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* @module
*/
import { capability, Schema, ok } from '@ucanto/validator'
import { content } from './blob.js'
import { content } from './space/blob.js'
import { equal, equalBody, equalWith, SpaceDID, Await, and } from './utils.js'

/**
Expand Down
Loading

0 comments on commit c810735

Please sign in to comment.