From d3b19ffa74d8dd0cae1b65e6e2a5f4b928771d2d Mon Sep 17 00:00:00 2001 From: Cafe137 Date: Thu, 24 Oct 2024 12:45:51 +0200 Subject: [PATCH 1/2] feat: support marshaled stamps --- src/bee.ts | 20 +++++++------------ src/chunk/cac.ts | 1 - src/chunk/soc.ts | 10 ++++------ src/modules/chunk.ts | 8 ++++---- src/modules/envelope.ts | 23 ++++++++------------- src/modules/feed.ts | 4 ++-- src/modules/soc.ts | 4 ++-- src/types/index.ts | 9 ++++++++- src/utils/headers.ts | 18 ++++++++++++----- src/utils/stamps.ts | 32 +++++++++++++++++++++++++++++- src/utils/type.ts | 4 ---- test/integration/bee-class.spec.ts | 27 +++++++++++++++---------- 12 files changed, 96 insertions(+), 64 deletions(-) diff --git a/src/bee.ts b/src/bee.ts index a045ac38..5f94ac14 100644 --- a/src/bee.ts +++ b/src/bee.ts @@ -21,7 +21,7 @@ import * as states from './modules/debug/states' import * as debugStatus from './modules/debug/status' import * as debugTag from './modules/debug/tag' import * as transactions from './modules/debug/transactions' -import { EnvelopeResponse, postEnvelope } from './modules/envelope' +import { postEnvelope } from './modules/envelope' import { createFeedManifest } from './modules/feed' import * as grantee from './modules/grantee' import * as pinning from './modules/pinning' @@ -45,6 +45,7 @@ import type { CollectionUploadOptions, Data, DebugStatus, + Envelope, ExtendedTag, FeedReader, FeedWriter, @@ -276,12 +277,11 @@ export class Bee { * @see [Bee API reference - `POST /chunks`](https://docs.ethswarm.org/api/#tag/Chunk/paths/~1chunks/post) */ async uploadChunk( - postageBatchId: string | BatchId, + stamp: BatchId | Uint8Array | string, data: Uint8Array, options?: UploadOptions, requestOptions?: BeeRequestOptions, ): Promise { - assertBatchId(postageBatchId) assertRequestOptions(requestOptions) if (!(data instanceof Uint8Array)) { @@ -300,7 +300,7 @@ export class Bee { assertUploadOptions(options) } - return chunk.upload(this.getRequestOptionsForCall(requestOptions), data, postageBatchId, options) + return chunk.upload(this.getRequestOptionsForCall(requestOptions), data, stamp, options) } /** @@ -1030,7 +1030,7 @@ export class Bee { * @see [Bee API reference - `POST /feeds`](https://docs.ethswarm.org/api/#tag/Feed/paths/~1feeds~1{owner}~1{topic}/post) */ async createFeedManifest( - postageBatchId: string | BatchId, + stamp: BatchId | Uint8Array | string, type: FeedType, topic: Topic | Uint8Array | string, owner: EthAddress | Uint8Array | string, @@ -1038,7 +1038,6 @@ export class Bee { ): Promise { assertRequestOptions(options) assertFeedType(type) - assertBatchId(postageBatchId) const canonicalTopic = makeTopic(topic) const canonicalOwner = makeHexEthAddress(owner) @@ -1047,7 +1046,7 @@ export class Bee { this.getRequestOptionsForCall(options), canonicalOwner, canonicalTopic, - postageBatchId, + stamp, ) return addCidConversionFunction({ reference }, ReferenceType.FEED) @@ -1234,16 +1233,11 @@ export class Bee { return { ...this.makeSOCReader(canonicalSigner.address, options), - upload: uploadSingleOwnerChunkData.bind(null, this.getRequestOptionsForCall(options), canonicalSigner), } } - async createEnvelope( - postageBatchId: BatchId, - reference: Reference, - options?: BeeRequestOptions, - ): Promise { + async createEnvelope(postageBatchId: BatchId, reference: Reference, options?: BeeRequestOptions): Promise { assertRequestOptions(options) return postEnvelope(this.getRequestOptionsForCall(options), postageBatchId, reference) diff --git a/src/chunk/cac.ts b/src/chunk/cac.ts index 0dfea9fc..dc88b738 100644 --- a/src/chunk/cac.ts +++ b/src/chunk/cac.ts @@ -24,7 +24,6 @@ export interface Chunk { readonly data: Uint8Array span(): Bytes<8> payload(): FlexBytes<1, 4096> - address(): PlainBytesReference } diff --git a/src/chunk/soc.ts b/src/chunk/soc.ts index b28dfc84..0b7a331e 100644 --- a/src/chunk/soc.ts +++ b/src/chunk/soc.ts @@ -15,7 +15,6 @@ import { BeeError } from '../utils/error' import { EthAddress } from '../utils/eth' import { keccak256Hash } from '../utils/hash' import { bytesToHex } from '../utils/hex' -import { assertAddress } from '../utils/type' import { bmtHash } from './bmt' import { Chunk, MAX_PAYLOAD_SIZE, MIN_PAYLOAD_SIZE, assertValidChunkData, makeContentAddressedChunk } from './cac' import { recoverAddress, sign } from './signer' @@ -136,7 +135,7 @@ export async function makeSingleOwnerChunk( export async function uploadSingleOwnerChunk( requestOptions: BeeRequestOptions, chunk: SingleOwnerChunk, - postageBatchId: BatchId, + stamp: BatchId | Uint8Array | string, options?: UploadOptions, ): Promise { const owner = bytesToHex(chunk.owner()) @@ -144,7 +143,7 @@ export async function uploadSingleOwnerChunk( const signature = bytesToHex(chunk.signature()) const data = Binary.concatBytes(chunk.span(), chunk.payload()) - return socAPI.upload(requestOptions, owner, identifier, signature, data, postageBatchId, options) + return socAPI.upload(requestOptions, owner, identifier, signature, data, stamp, options) } /** @@ -160,16 +159,15 @@ export async function uploadSingleOwnerChunk( export async function uploadSingleOwnerChunkData( requestOptions: BeeRequestOptions, signer: Signer, - postageBatchId: BatchId | string, + stamp: BatchId | Uint8Array | string, identifier: Identifier, data: Uint8Array, options?: UploadOptions, ): Promise { - assertAddress(postageBatchId) const cac = makeContentAddressedChunk(data) const soc = await makeSingleOwnerChunk(cac, identifier, signer) - return uploadSingleOwnerChunk(requestOptions, soc, postageBatchId, options) + return uploadSingleOwnerChunk(requestOptions, soc, stamp, options) } /** diff --git a/src/modules/chunk.ts b/src/modules/chunk.ts index 725ec139..ab2ea4d8 100644 --- a/src/modules/chunk.ts +++ b/src/modules/chunk.ts @@ -22,14 +22,14 @@ const endpoint = 'chunks' * Upload expects the chuck data to be set accordingly. * * @param requestOptions Options for making requests - * @param data Chunk data to be uploaded - * @param postageBatchId Postage BatchId that will be assigned to uploaded data + * @param data Chunk data to be uploaded + * @param stamp BatchId or marshaled stamp to be used for the upload * @param options Additional options like tag, encryption, pinning */ export async function upload( requestOptions: BeeRequestOptions, data: Uint8Array, - postageBatchId: BatchId, + stamp: BatchId | Uint8Array | string, options?: UploadOptions, ): Promise { const response = await http(requestOptions, { @@ -38,7 +38,7 @@ export async function upload( data, headers: { 'content-type': 'application/octet-stream', - ...extractUploadHeaders(postageBatchId, options), + ...extractUploadHeaders(stamp, options), }, responseType: 'json', }) diff --git a/src/modules/envelope.ts b/src/modules/envelope.ts index 7b964e1d..e39540e4 100644 --- a/src/modules/envelope.ts +++ b/src/modules/envelope.ts @@ -1,22 +1,15 @@ -import { Types } from 'cafe-utility' -import type { BatchId, BeeRequestOptions, Reference } from '../types' +import { Binary, Types } from 'cafe-utility' +import type { BatchId, BeeRequestOptions, Envelope, Reference } from '../types' import { http } from '../utils/http' const ENVELOPE_ENDPOINT = 'envelope' -export interface EnvelopeResponse { - issuer: string - index: string - timestamp: string - signature: string -} - export async function postEnvelope( requestOptions: BeeRequestOptions, postageBatchId: BatchId, reference: Reference, -): Promise { - const response = await http(requestOptions, { +): Promise { + const { data } = await http(requestOptions, { method: 'post', responseType: 'json', url: `${ENVELOPE_ENDPOINT}/${reference}`, @@ -26,9 +19,9 @@ export async function postEnvelope( }) return { - issuer: Types.asHexString(response.data.issuer, { name: 'issuer', byteLength: 20 }), - index: Types.asHexString(response.data.index, { name: 'index', byteLength: 8 }), - timestamp: Types.asHexString(response.data.timestamp, { name: 'timestamp', byteLength: 8 }), - signature: Types.asHexString(response.data.signature, { name: 'signature', byteLength: 65 }), + issuer: Binary.hexToUint8Array(Types.asHexString(data.issuer, { name: 'issuer', byteLength: 20 })), + index: Binary.hexToUint8Array(Types.asHexString(data.index, { name: 'index', byteLength: 8 })), + timestamp: Binary.hexToUint8Array(Types.asHexString(data.timestamp, { name: 'timestamp', byteLength: 8 })), + signature: Binary.hexToUint8Array(Types.asHexString(data.signature, { name: 'signature', byteLength: 65 })), } } diff --git a/src/modules/feed.ts b/src/modules/feed.ts index 1e3de280..570b7b8f 100644 --- a/src/modules/feed.ts +++ b/src/modules/feed.ts @@ -53,13 +53,13 @@ export async function createFeedManifest( requestOptions: BeeRequestOptions, owner: HexEthAddress, topic: Topic, - postageBatchId: BatchId, + stamp: BatchId | Uint8Array | string, ): Promise { const response = await http(requestOptions, { method: 'post', responseType: 'json', url: `${feedEndpoint}/${owner}/${topic}`, - headers: extractUploadHeaders(postageBatchId), + headers: extractUploadHeaders(stamp), }) return response.data.reference diff --git a/src/modules/soc.ts b/src/modules/soc.ts index 2c455071..c74c0b3d 100644 --- a/src/modules/soc.ts +++ b/src/modules/soc.ts @@ -22,7 +22,7 @@ export async function upload( identifier: string, signature: string, data: Uint8Array, - postageBatchId: BatchId, + stamp: BatchId | Uint8Array | string, options?: UploadOptions, ): Promise { const response = await http(requestOptions, { @@ -31,7 +31,7 @@ export async function upload( data, headers: { 'content-type': 'application/octet-stream', - ...extractUploadHeaders(postageBatchId, options), + ...extractUploadHeaders(stamp, options), }, responseType: 'json', params: { sig: signature }, diff --git a/src/types/index.ts b/src/types/index.ts index 8a8ba574..c44236d8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -529,7 +529,7 @@ export interface SOCWriter extends SOCReader { * @param options Upload options */ upload: ( - postageBatchId: string | BatchId, + stamp: BatchId | Uint8Array | string, identifier: Identifier, data: Uint8Array, options?: UploadOptions, @@ -617,6 +617,13 @@ export interface PostageBatchOptions { waitForUsableTimeout?: number } +export interface Envelope { + issuer: Uint8Array + index: Uint8Array + timestamp: Uint8Array + signature: Uint8Array +} + /** * With this type a number should be represented in a string */ diff --git a/src/utils/headers.ts b/src/utils/headers.ts index 2607650f..4a9dd51e 100644 --- a/src/utils/headers.ts +++ b/src/utils/headers.ts @@ -1,3 +1,4 @@ +import { Binary } from 'cafe-utility' import { BatchId, DownloadRedundancyOptions, FileHeaders, UploadOptions, UploadRedundancyOptions } from '../types' import { BeeError } from './error' @@ -44,13 +45,20 @@ export function readFileHeaders(headers: Record): FileHeaders { } } -export function extractUploadHeaders(postageBatchId: BatchId, options?: UploadOptions): Record { - if (!postageBatchId) { - throw new BeeError('Postage BatchID has to be specified!') +export function extractUploadHeaders( + stamp: BatchId | Uint8Array | string, + options?: UploadOptions, +): Record { + if (!stamp) { + throw new BeeError('Stamp has to be specified!') } - const headers: Record = { - 'swarm-postage-batch-id': postageBatchId, + const headers: Record = {} + + if (stamp instanceof Uint8Array) { + headers['swarm-postage-stamp'] = Binary.uint8ArrayToHex(stamp) + } else { + headers['swarm-postage-batch-id'] = stamp } if (options?.act) { diff --git a/src/utils/stamps.ts b/src/utils/stamps.ts index cb7be9a8..f48c2b1e 100644 --- a/src/utils/stamps.ts +++ b/src/utils/stamps.ts @@ -1,4 +1,5 @@ -import { NumberString } from '../types' +import { Binary } from 'cafe-utility' +import { BatchId, Envelope, NumberString } from '../types' /** * Utility function that calculates usage of postage batch based on its utilization, depth and bucket depth. @@ -114,3 +115,32 @@ export function getAmountForTtl(days: number): NumberString { export function getDepthForCapacity(gigabytes: number): number { return gigabytes <= 1 ? 18 : Math.ceil(Math.log2(Math.ceil(gigabytes)) + 18) } + +export function convertEnvelopeToMarshaledStamp(batchID: BatchId, envelope: Envelope): Uint8Array { + return marshalStamp(envelope.signature, Binary.hexToUint8Array(batchID), envelope.timestamp, envelope.index) +} + +export function marshalStamp( + signature: Uint8Array, + batchID: Uint8Array, + timestamp: Uint8Array, + index: Uint8Array, +): Uint8Array { + if (signature.length !== 65) { + throw Error('invalid signature length') + } + + if (batchID.length !== 32) { + throw Error('invalid batch ID length') + } + + if (timestamp.length !== 8) { + throw Error('invalid signature length') + } + + if (index.length !== 8) { + throw Error('invalid signature length') + } + + return Binary.concatBytes(batchID, index, timestamp, signature) +} diff --git a/src/utils/type.ts b/src/utils/type.ts index 9b889392..5f694299 100644 --- a/src/utils/type.ts +++ b/src/utils/type.ts @@ -36,10 +36,6 @@ export function isReadable(obj: unknown): obj is Readable { return typeof Readable !== 'undefined' && obj instanceof Readable } -export function isUint8Array(obj: unknown): obj is Uint8Array { - return obj instanceof Uint8Array -} - export function isInteger(value: unknown): value is number | NumberString { return ( (typeof value === 'string' && /^-?(0|[1-9][0-9]*)$/g.test(value)) || diff --git a/test/integration/bee-class.spec.ts b/test/integration/bee-class.spec.ts index 648984da..a813a76d 100644 --- a/test/integration/bee-class.spec.ts +++ b/test/integration/bee-class.spec.ts @@ -1,11 +1,13 @@ -import { System } from 'cafe-utility' +import { Binary, System } from 'cafe-utility' import { Bee, BytesReference, Collection, PssSubscription } from '../../src' +import { makeContentAddressedChunk } from '../../src/chunk/cac' import { makeSigner } from '../../src/chunk/signer' import { uploadSingleOwnerChunkData } from '../../src/chunk/soc' import * as bzz from '../../src/modules/bzz' -import { REFERENCE_HEX_LENGTH } from '../../src/types' +import { Reference, REFERENCE_HEX_LENGTH } from '../../src/types' import { makeBytes } from '../../src/utils/bytes' -import { HexString, bytesToHex } from '../../src/utils/hex' +import { bytesToHex, HexString } from '../../src/utils/hex' +import { convertEnvelopeToMarshaledStamp } from '../../src/utils/stamps' import { beeKyOptions, beePeerUrl, @@ -613,14 +615,19 @@ describe('Bee class', () => { }) }) - describe('Envelope', () => { + describe('POST /envelope', () => { it('should post envelope', async function () { - const uploadResult = await bee.uploadData(getPostageBatch(), 'Envelope') - const envelopeResult = await bee.createEnvelope(getPostageBatch(), uploadResult.reference) - expect(envelopeResult.index).toHaveLength(16) - expect(envelopeResult.issuer).toHaveLength(40) - expect(envelopeResult.signature).toHaveLength(130) - expect(envelopeResult.timestamp).toHaveLength(16) + const payload = new TextEncoder().encode('Marshaled upload') + const cac = makeContentAddressedChunk(payload) + const address = Binary.uint8ArrayToHex(cac.address()) as Reference + const envelope = await bee.createEnvelope(getPostageBatch(), address) + expect(envelope.index).toHaveLength(8) + expect(envelope.issuer).toHaveLength(20) + expect(envelope.signature).toHaveLength(65) + expect(envelope.timestamp).toHaveLength(8) + const stamp = convertEnvelopeToMarshaledStamp(getPostageBatch(), envelope) + const chunk = await bee.uploadChunk(stamp, cac.data) + expect(chunk.reference).toBe(address) }) }) From 8e74c4a3aa2bf70547ba909a0aaee2ac341c65a9 Mon Sep 17 00:00:00 2001 From: Cafe137 Date: Thu, 24 Oct 2024 12:48:46 +0200 Subject: [PATCH 2/2] fix: fix field names in error --- src/utils/stamps.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/stamps.ts b/src/utils/stamps.ts index f48c2b1e..25c37dd6 100644 --- a/src/utils/stamps.ts +++ b/src/utils/stamps.ts @@ -135,11 +135,11 @@ export function marshalStamp( } if (timestamp.length !== 8) { - throw Error('invalid signature length') + throw Error('invalid timestamp length') } if (index.length !== 8) { - throw Error('invalid signature length') + throw Error('invalid index length') } return Binary.concatBytes(batchID, index, timestamp, signature)