Skip to content

Commit

Permalink
feat(billing): support blob capabilities (#356)
Browse files Browse the repository at this point in the history
* Use the blob allocate receipt to track space size additions.
* Use blob remove receipt to track space size reductions.
  • Loading branch information
Alan Shaw authored Apr 24, 2024
1 parent af4700c commit 5779161
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 19 deletions.
42 changes: 39 additions & 3 deletions billing/lib/ucan-stream.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as ServiceBlobCaps from '@web3-storage/capabilities/web3.storage/blob'
import * as BlobCaps from '@web3-storage/capabilities/blob'
import * as StoreCaps from '@web3-storage/capabilities/store'

/**
Expand All @@ -13,23 +15,35 @@ export const findSpaceUsageDeltas = messages => {
for (const message of messages) {
if (!isReceipt(message)) continue

/** @type {import('@ucanto/interface').DID|undefined} */
let resource
/** @type {number|undefined} */
let size
if (isReceiptForCapability(message, StoreCaps.add) && isStoreAddSuccess(message.out)) {
if (isReceiptForCapability(message, ServiceBlobCaps.allocate) && isServiceBlobAllocateSuccess(message.out)) {
resource = message.value.att[0].nb?.space
size = message.out.ok.size
} else if (isReceiptForCapability(message, BlobCaps.remove) && isBlobRemoveSuccess(message.out)) {
resource = /** @type {import('@ucanto/interface').DID} */ (message.value.att[0].with)
size = -message.out.ok.size
// TODO: remove me LEGACY store/add
} else if (isReceiptForCapability(message, StoreCaps.add) && isStoreAddSuccess(message.out)) {
resource = /** @type {import('@ucanto/interface').DID} */ (message.value.att[0].with)
size = message.out.ok.allocated
// TODO: remove me LEGACY store/remove
} else if (isReceiptForCapability(message, StoreCaps.remove) && isStoreRemoveSuccess(message.out)) {
resource = /** @type {import('@ucanto/interface').DID} */ (message.value.att[0].with)
size = -message.out.ok.size
}

// Is message is a repeat store/add for the same shard or not a valid
// store/add or store/remove receipt?
if (size == 0 || size == null) {
if (resource == null || size == 0 || size == null) {
continue
}

/** @type {import('./api.js').UsageDelta} */
const delta = {
resource: /** @type {import('@ucanto/interface').DID} */ (message.value.att[0].with),
resource,
cause: message.invocationCid,
delta: size,
// TODO: use receipt timestamp per https://github.com/web3-storage/w3up/issues/970
Expand Down Expand Up @@ -83,6 +97,28 @@ export const storeSpaceUsageDelta = async (delta, ctx) => {
*/
const isReceipt = m => m.type === 'receipt'

/**
* @param {import('@ucanto/interface').Result} r
* @returns {r is { ok: import('@web3-storage/capabilities/types').BlobAllocateSuccess }}
*/
const isServiceBlobAllocateSuccess = r =>
!r.error &&
r.ok != null &&
typeof r.ok === 'object' &&
'size' in r.ok &&
(typeof r.ok.size === 'number')

/**
* @param {import('@ucanto/interface').Result} r
* @returns {r is { ok: import('@web3-storage/capabilities/types').BlobRemoveSuccess }}
*/
const isBlobRemoveSuccess = r =>
!r.error &&
r.ok != null &&
typeof r.ok === 'object' &&
'size' in r.ok &&
(typeof r.ok.size === 'number')

/**
* @param {import('@ucanto/interface').Result} r
* @returns {r is { ok: import('@web3-storage/capabilities/types').StoreAddSuccess }}
Expand Down
2 changes: 1 addition & 1 deletion billing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"@sentry/serverless": "^7.74.1",
"@ucanto/interface": "^10.0.1",
"@ucanto/server": "^10.0.0",
"@web3-storage/capabilities": "^13.3.1",
"@web3-storage/capabilities": "^14.0.0",
"big.js": "^6.2.1",
"multiformats": "^13.1.0",
"p-retry": "^6.2.0",
Expand Down
7 changes: 5 additions & 2 deletions billing/test/helpers/did.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ const randomDomain = () =>
export const randomDIDMailto = () =>
`did:mailto:${randomDomain()}:${randomAlphas(randomInteger(1, 16))}`

/** @returns {Promise<import("@ucanto/interface").DID>} */
export const randomDID = async () => {
/** @returns {Promise<import("@ucanto/interface").DID>} */
export const randomDID = () => randomDIDKey()

/** @returns {Promise<import("@ucanto/interface").DID<'key'>>} */
export const randomDIDKey = async () => {
const signer = await Signer.generate()
return signer.did()
}
76 changes: 64 additions & 12 deletions billing/test/lib/ucan-stream.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Schema } from '@ucanto/core'
import * as ServiceBlobCaps from '@web3-storage/capabilities/web3.storage/blob'
import * as BlobCaps from '@web3-storage/capabilities/blob'
import * as StoreCaps from '@web3-storage/capabilities/store'
import { findSpaceUsageDeltas, storeSpaceUsageDelta } from '../../lib/ucan-stream.js'
import { randomConsumer } from '../helpers/consumer.js'
import { randomLink } from '../helpers/dag.js'
import { randomDID } from '../helpers/did.js'
import { randomDID, randomDIDKey } from '../helpers/did.js'

/** @type {import('./api').TestSuite<import('./api').UCANStreamTestContext>} */
export const test = {
Expand All @@ -14,7 +17,7 @@ export const test = {
value: {
att: [{
with: await randomDID(),
can: 'store/list'
can: StoreCaps.list.can
}],
aud: await randomDID(),
cid: randomLink()
Expand All @@ -24,15 +27,61 @@ export const test = {

const shard = randomLink()

/** @type {import('../../lib/api.js').UcanReceiptMessage[]} */
/**
* @type {import('../../lib/api.js').UcanReceiptMessage<[
* | import('@web3-storage/capabilities/types').BlobAllocate
* | import('@web3-storage/capabilities/types').BlobRemove
* | import('@web3-storage/capabilities/types').StoreAdd
* | import('@web3-storage/capabilities/types').StoreRemove
* ]>[]}
*/
const receipts = [{
type: 'receipt',
carCid: randomLink(),
invocationCid: randomLink(),
value: {
att: [{
with: await randomDID(),
can: 'store/add',
with: await randomDIDKey(),
can: ServiceBlobCaps.allocate.can,
nb: {
blob: {
digest: randomLink().multihash.bytes,
size: 138
},
cause: randomLink(),
space: await randomDIDKey()
}
}],
aud: await randomDID(),
cid: randomLink()
},
out: { ok: { size: 138 } },
ts: new Date()
}, {
type: 'receipt',
carCid: randomLink(),
invocationCid: randomLink(),
value: {
att: [{
with: await randomDIDKey(),
can: BlobCaps.remove.can,
nb: {
digest: randomLink().multihash.bytes
}
}],
aud: await randomDID(),
cid: randomLink()
},
out: { ok: { size: 138 } },
ts: new Date()
}, {
type: 'receipt',
carCid: randomLink(),
invocationCid: randomLink(),
value: {
att: [{
with: await randomDIDKey(),
can: StoreCaps.add.can,
nb: {
link: shard,
size: 138
Expand All @@ -49,8 +98,8 @@ export const test = {
invocationCid: randomLink(),
value: {
att: [{
with: await randomDID(),
can: 'store/remove',
with: await randomDIDKey(),
can: StoreCaps.remove.can,
nb: { link: shard }
}],
aud: await randomDID(),
Expand All @@ -66,8 +115,11 @@ export const test = {
// ensure we have a delta for every receipt
for (const r of receipts) {
assert.ok(deltas.some(d => (
d.resource === r.value.att[0].with &&
d.cause.toString() === r.invocationCid.toString()
d.cause.toString() === r.invocationCid.toString() &&
// resource for blob allocate is found in the caveats
(r.value.att[0].can === ServiceBlobCaps.allocate.can
? d.resource === r.value.att[0].nb.space
: d.resource === r.value.att[0].with)
)))
}
},
Expand All @@ -86,7 +138,7 @@ export const test = {
value: {
att: [{
with: Schema.did({ method: 'key' }).from(consumer.consumer),
can: 'store/add',
can: StoreCaps.add.can,
nb: {
link: randomLink(),
size: 138
Expand All @@ -104,7 +156,7 @@ export const test = {
value: {
att: [{
with: Schema.did({ method: 'key' }).from(consumer.consumer),
can: 'store/add',
can: StoreCaps.add.can,
nb: {
link: randomLink(),
size: 1138
Expand Down Expand Up @@ -158,7 +210,7 @@ export const test = {
value: {
att: [{
with: Schema.did({ method: 'key' }).from(consumer.consumer),
can: 'store/add',
can: StoreCaps.add.can,
nb: {
link: randomLink(),
size: 138
Expand Down
43 changes: 42 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 5779161

Please sign in to comment.