Skip to content

Commit

Permalink
fix: reuse http/put and blob/accept receipts when available (#1495)
Browse files Browse the repository at this point in the history
From @vasco-santos:
1. if we already have a http/put receipt, we should not send one
2. if we already receive in next a blob accept receipt, we should not
await on the receipt generation
  • Loading branch information
joaosa authored Jun 5, 2024
1 parent 7e97090 commit 416802e
Show file tree
Hide file tree
Showing 4 changed files with 286 additions and 71 deletions.
58 changes: 31 additions & 27 deletions packages/upload-client/src/blob.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,15 +215,15 @@ export async function add(

const nextTasks = parseBlobAddReceiptNext(result)

const { receipt } = nextTasks.allocate
const { receipt: allocateReceipt } = nextTasks.allocate
/* c8 ignore next 5 */
if (!receipt.out.ok) {
if (!allocateReceipt.out.ok) {
throw new Error(`failed ${BlobCapabilities.add.can} invocation`, {
cause: receipt.out.error,
cause: allocateReceipt.out.error,
})
}

const { address } = receipt.out.ok
const { address } = allocateReceipt.out.ok
if (address) {
const fetchWithUploadProgress =
options.fetchWithUploadProgress ||
Expand Down Expand Up @@ -280,32 +280,36 @@ export async function add(
}

// Invoke `conclude` with `http/put` receipt
const derivedSigner = ed25519.from(
/** @type {import('@ucanto/interface').SignerArchive<import('@ucanto/interface').DID, typeof ed25519.signatureCode>} */
(nextTasks.put.task.facts[0]['keys'])
)

const httpPutReceipt = await Receipt.issue({
issuer: derivedSigner,
ran: nextTasks.put.task.cid,
result: { ok: {} },
})
const httpPutConcludeInvocation = createConcludeInvocation(
issuer,
// @ts-expect-error object of type unknown
audience,
httpPutReceipt
)
const ucanConclude = await httpPutConcludeInvocation.execute(conn)

if (!ucanConclude.out.ok) {
throw new Error(`failed ${BlobCapabilities.add.can} invocation`, {
cause: result.out.error,
let { receipt: httpPutReceipt } = nextTasks.put
if (!httpPutReceipt?.out.ok) {
const derivedSigner = ed25519.from(
/** @type {import('@ucanto/interface').SignerArchive<import('@ucanto/interface').DID, typeof ed25519.signatureCode>} */
(nextTasks.put.task.facts[0]['keys'])
)
httpPutReceipt = await Receipt.issue({
issuer: derivedSigner,
ran: nextTasks.put.task.cid,
result: { ok: {} },
})
const httpPutConcludeInvocation = createConcludeInvocation(
issuer,
// @ts-expect-error object of type unknown
audience,
httpPutReceipt
)
const ucanConclude = await httpPutConcludeInvocation.execute(conn)
if (!ucanConclude.out.ok) {
throw new Error(`failed ${BlobCapabilities.add.can} invocation`, {
cause: result.out.error,
})
}
}

// Ensure the blob has been accepted
const acceptReceipt = await poll(nextTasks.accept.task.link(), options)
let { receipt: acceptReceipt } = nextTasks.accept
if (!acceptReceipt?.out.ok) {
acceptReceipt = await poll(nextTasks.accept.task.link(), options)
}

const blocks = new Map(
[...acceptReceipt.iterateIPLDBlocks()].map((block) => [
Expand All @@ -315,7 +319,7 @@ export async function add(
)
const site = Delegation.view({
root: /** @type {import('@ucanto/interface').UCANLink} */ (
acceptReceipt.out.ok.site
acceptReceipt.out.ok?.site
),
blocks,
})
Expand Down
132 changes: 132 additions & 0 deletions packages/upload-client/test/blob.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
setupBlobAddSuccessResponse,
setupBlobAdd4xxResponse,
setupBlobAdd5xxResponse,
setupBlobAddWithAcceptReceiptSuccessResponse,
setupBlobAddWithHttpPutReceiptSuccessResponse,
receiptsEndpoint,
} from './helpers/utils.js'
import { fetchWithUploadProgress } from '../src/fetch-with-upload-progress.js'
Expand Down Expand Up @@ -308,6 +310,136 @@ describe('Blob.add', () => {
)
})

it('reuses the blob/accept receipt when it is already available', async () => {
const space = await Signer.generate()
const agent = await Signer.generate()
const bytes = await randomBytes(128)
const bytesHash = await sha256.digest(bytes)

const proofs = [
await BlobCapabilities.add.delegate({
issuer: space,
audience: agent,
with: space.did(),
expiration: Infinity,
}),
]

const service = mockService({
ucan: {
conclude: provide(UCAN.conclude, () => {
return { ok: { time: Date.now() } }
}),
},
space: {
blob: {
// @ts-ignore Argument of type
add: provide(BlobCapabilities.add, ({ invocation }) => {
return setupBlobAddWithAcceptReceiptSuccessResponse(
{ issuer: space, audience: agent, with: space, proofs },
invocation
)
}),
},
},
})

const server = Server.create({
id: serviceSigner,
service,
codec: CAR.inbound,
validateAuthorization,
})
const connection = Client.connect({
id: serviceSigner,
codec: CAR.outbound,
channel: server,
})

const { site, multihash } = await Blob.add(
{ issuer: agent, with: space.did(), proofs, audience: serviceSigner },
bytes,
{
connection,
receiptsEndpoint,
}
)

assert(multihash)
assert.deepEqual(multihash, bytesHash)

assert(site)
assert.equal(site.capabilities[0].can, Assert.location.can)
// we're not verifying this as it's a mocked value
// @ts-ignore nb unknown
assert.ok(site.capabilities[0].nb.content.multihash.bytes)
})

it('reuses the http/put receipt when it is already available', async () => {
const space = await Signer.generate()
const agent = await Signer.generate()
const bytes = await randomBytes(128)
const bytesHash = await sha256.digest(bytes)

const proofs = [
await BlobCapabilities.add.delegate({
issuer: space,
audience: agent,
with: space.did(),
expiration: Infinity,
}),
]

const service = mockService({
ucan: {
conclude: provide(UCAN.conclude, () => {
return { ok: { time: Date.now() } }
}),
},
space: {
blob: {
// @ts-ignore Argument of type
add: provide(BlobCapabilities.add, ({ invocation }) => {
return setupBlobAddWithHttpPutReceiptSuccessResponse(
{ issuer: space, audience: agent, with: space, proofs },
invocation
)
}),
},
},
})

const server = Server.create({
id: serviceSigner,
service,
codec: CAR.inbound,
validateAuthorization,
})
const connection = Client.connect({
id: serviceSigner,
codec: CAR.outbound,
channel: server,
})

const { site, multihash } = await Blob.add(
{ issuer: agent, with: space.did(), proofs, audience: serviceSigner },
bytes,
{
connection,
receiptsEndpoint,
}
)

assert(multihash)
assert.deepEqual(multihash, bytesHash)

assert(site)
assert.equal(site.capabilities[0].can, Assert.location.can)
// we're not verifying this as it's a mocked value
// @ts-ignore nb unknown
assert.ok(site.capabilities[0].nb.content.multihash.bytes)
})

it('throws for bucket URL client error 4xx', async () => {
const space = await Signer.generate()
const agent = await Signer.generate()
Expand Down
41 changes: 7 additions & 34 deletions packages/upload-client/test/helpers/receipts-server.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,16 @@
import { createServer } from 'http'
import { parseLink } from '@ucanto/server'
import * as Signer from '@ucanto/principal/ed25519'
import { Receipt, Message } from '@ucanto/core'
import * as CAR from '@ucanto/transport/car'
import { Assert } from '@web3-storage/content-claims/capability'
import { Message } from '@ucanto/core'
import { createServer } from 'http'
import { randomCAR } from './random.js'
import { generateAcceptReceipt } from '../helpers/utils.js'

const port = process.env.PORT ?? 9201

/**
* @param {string} taskCid
*/
const generateReceipt = async (taskCid) => {
const issuer = await Signer.generate()
const content = (await randomCAR(128)).cid
const locationClaim = await Assert.location.delegate({
issuer,
audience: issuer,
with: issuer.toDIDKey(),
nb: {
content,
location: ['http://localhost'],
},
expiration: Infinity,
})

const receipt = await Receipt.issue({
issuer,
fx: {
fork: [locationClaim],
},
ran: parseLink(taskCid),
result: {
ok: {
site: locationClaim.link(),
},
},
})

const encodeReceipt = async (taskCid) => {
const receipt = await generateAcceptReceipt(taskCid)
const message = await Message.build({
receipts: [receipt],
})
Expand All @@ -54,11 +27,11 @@ const server = createServer(async (req, res) => {
res.writeHead(404)
res.end()
} else if (taskCid === 'failed') {
const body = await generateReceipt((await randomCAR(128)).cid.toString())
const body = await encodeReceipt((await randomCAR(128)).cid.toString())
res.writeHead(200)
res.end(body)
} else {
const body = await generateReceipt(taskCid)
const body = await encodeReceipt(taskCid)
res.writeHead(200)
res.end(body)
}
Expand Down
Loading

0 comments on commit 416802e

Please sign in to comment.