Skip to content

Commit

Permalink
refactor!: breaking api changes (#105)
Browse files Browse the repository at this point in the history
* refactor: breaking api changes

* refactor: add owner to SOC interface

* feat: add /soc endpoint module

* fix: soc owner calculation

* chore: fix linter issues

* fix: add ReferenceResponse to types

* fix: comment out beeinfra version change

* chore: cleanup

* docs: add comment to CI script

* fix: workaround for settlements bug

ethersphere/bee#1212

* refactor: requested changes

* fix: settlements bug

* test: change ci to run Bee 0.5.0
  • Loading branch information
agazso committed Feb 9, 2021
1 parent 518eb7d commit 5eb1d15
Show file tree
Hide file tree
Showing 13 changed files with 149 additions and 114 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
export URL=$(curl -s https://api.github.com/repos/ethersphere/bee-local/releases/latest | jq -r .tarball_url)
curl -Ls ${URL} -o bee-local.tar.gz
tar --strip-components=1 --wildcards -xzf bee-local.tar.gz ethersphere-bee-local-*/{beeinfra.sh,helm-values,hack}
sed -i 's/IMAGE_TAG="latest"/IMAGE_TAG="0.4.2"/' beeinfra.sh
sed -i 's/IMAGE_TAG="latest"/IMAGE_TAG="0.5.0"/' beeinfra.sh
- name: Install latest beekeeper
run: |
export TAG=$(curl -s https://api.github.com/repos/ethersphere/beekeeper/releases/latest | jq -r .tag_name)
Expand Down
85 changes: 53 additions & 32 deletions src/chunk/soc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { keccak256Hash } from './hash'
import { SPAN_SIZE } from './span'
import { serializeBytes } from './serialize'
import { BeeError } from '../utils/error'
import { BrandedType } from '../types'
import { Chunk, ChunkAddress, MAX_PAYLOAD_SIZE, MIN_PAYLOAD_SIZE, verifyChunk } from './cac'
import { ReferenceResponse, UploadOptions } from '../types'
import { bytesToHex } from '../utils/hex'
import * as socAPI from '../modules/soc'

const IDENTIFIER_SIZE = 32
const SIGNATURE_SIZE = 65
Expand All @@ -29,33 +31,18 @@ export type Identifier = Bytes<32>
export interface SingleOwnerChunk extends Chunk {
identifier: () => Identifier
signature: () => Signature
owner: () => EthAddress
}

type ValidSingleOwnerChunkData = BrandedType<Uint8Array, 'ValidSingleOwnerChunkData'>
function recoverChunkOwner(data: Uint8Array): EthAddress {
const cacData = data.slice(SOC_SPAN_OFFSET)
const chunkAddress = bmtHash(cacData)
const signature = verifyBytesAtOffset(SOC_SIGNATURE_OFFSET, SIGNATURE_SIZE, data)
const identifier = verifyBytesAtOffset(SOC_IDENTIFIER_OFFSET, IDENTIFIER_SIZE, data)
const digest = keccak256Hash(identifier, chunkAddress)
const ownerAddress = recoverAddress(signature, digest)

/**
* Type guard for valid single owner chunk data
*
* @param data The chunk data
* @param address The address of the single owner chunk
*/
export function isValidSingleOwnerChunkData(
data: Uint8Array,
address: ChunkAddress,
): data is ValidSingleOwnerChunkData {
try {
const cacData = data.slice(SOC_SPAN_OFFSET)
const chunkAddress = bmtHash(cacData)
const signature = verifyBytesAtOffset(SOC_SIGNATURE_OFFSET, SIGNATURE_SIZE, data)
const identifier = verifyBytesAtOffset(SOC_IDENTIFIER_OFFSET, IDENTIFIER_SIZE, data)
const digest = keccak256Hash(identifier, chunkAddress)
const accountAddress = recoverAddress(signature, digest)
const socAddress = keccak256Hash(identifier, accountAddress)

return bytesEqual(address, socAddress)
} catch (e) {
return false
}
return ownerAddress
}

/**
Expand All @@ -67,22 +54,32 @@ export function isValidSingleOwnerChunkData(
* @returns a single owner chunk or throws error
*/
export function verifySingleOwnerChunk(data: Uint8Array, address: ChunkAddress): SingleOwnerChunk {
if (isValidSingleOwnerChunkData(data, address)) {
return makeSingleOwnerChunkFromData(data, address)
const ownerAddress = recoverChunkOwner(data)
const identifier = verifyBytesAtOffset(SOC_IDENTIFIER_OFFSET, IDENTIFIER_SIZE, data)
const socAddress = keccak256Hash(identifier, ownerAddress)

if (!bytesEqual(address, socAddress)) {
throw new BeeError('verifySingleOwnerChunk')
}
throw new BeeError('verifySingleOwnerChunk')

return makeSingleOwnerChunkFromData(data, address, ownerAddress)
}

function verifyBytesAtOffset<Length extends number>(offset: number, length: Length, data: Uint8Array): Bytes<Length> {
return verifyBytes(length, bytesAtOffset(offset, length, data))
}

function makeSingleOwnerChunkFromData(data: ValidSingleOwnerChunkData, chunkAddress: ChunkAddress): SingleOwnerChunk {
function makeSingleOwnerChunkFromData(
data: Uint8Array,
socAddress: ChunkAddress,
ownerAddress: EthAddress,
): SingleOwnerChunk {
const identifier = () => bytesAtOffset(SOC_IDENTIFIER_OFFSET, IDENTIFIER_SIZE, data)
const signature = () => bytesAtOffset(SOC_SIGNATURE_OFFSET, SIGNATURE_SIZE, data)
const span = () => bytesAtOffset(SOC_SPAN_OFFSET, SPAN_SIZE, data)
const payload = () => flexBytesAtOffset(SOC_PAYLOAD_OFFSET, MIN_PAYLOAD_SIZE, MAX_PAYLOAD_SIZE, data)
const address = () => chunkAddress
const address = () => socAddress
const owner = () => ownerAddress

return {
data,
Expand All @@ -91,6 +88,7 @@ function makeSingleOwnerChunkFromData(data: ValidSingleOwnerChunkData, chunkAddr
span,
payload,
address,
owner,
}
}

Expand All @@ -115,8 +113,31 @@ export async function makeSingleOwnerChunk(

const digest = keccak256Hash(identifier, chunkAddress)
const signature = await sign(digest, signer)
const data = serializeBytes(identifier, signature, chunk.span(), chunk.payload()) as ValidSingleOwnerChunkData
const data = serializeBytes(identifier, signature, chunk.span(), chunk.payload())
const address = keccak256Hash(identifier, signer.address)
const soc = makeSingleOwnerChunkFromData(data, address, signer.address)

return soc
}

/**
* Helper function to upload a chunk.
*
* It uses the Chunk API and calculates the address before uploading.
*
* @param url The url of the Bee service
* @param chunk A chunk object
* @param options Upload options
*/
export function uploadSingleOwnerChunk(
url: string,
chunk: SingleOwnerChunk,
options?: UploadOptions,
): Promise<ReferenceResponse> {
const owner = bytesToHex(chunk.owner())
const identifier = bytesToHex(chunk.identifier())
const signature = bytesToHex(chunk.signature())
const data = serializeBytes(chunk.span(), chunk.payload())

return makeSingleOwnerChunkFromData(data, address)
return socAPI.upload(url, owner, identifier, signature, data, options)
}
20 changes: 0 additions & 20 deletions src/chunk/upload.ts

This file was deleted.

16 changes: 5 additions & 11 deletions src/modules/chunk.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Readable } from 'stream'
import type { BeeResponse, UploadOptions } from '../types'
import { prepareData } from '../utils/data'
import type { ReferenceResponse, UploadOptions } from '../types'
import { extractUploadHeaders } from '../utils/headers'
import { safeAxios } from '../utils/safeAxios'

Expand All @@ -18,16 +17,11 @@ const endpoint = '/chunks'
* @param data Chunk data to be uploaded
* @param options Aditional options like tag, encryption, pinning
*/
export async function upload(
url: string,
hash: string,
data: Uint8Array,
options?: UploadOptions,
): Promise<BeeResponse> {
const response = await safeAxios<BeeResponse>({
export async function upload(url: string, data: Uint8Array, options?: UploadOptions): Promise<ReferenceResponse> {
const response = await safeAxios<ReferenceResponse>({
method: 'post',
url: `${url}${endpoint}/${hash}`,
data: await prepareData(data),
url: `${url}${endpoint}`,
data,
headers: {
'content-type': 'application/octet-stream',
...extractUploadHeaders(options),
Expand Down
38 changes: 38 additions & 0 deletions src/modules/soc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ReferenceResponse, UploadOptions } from '../types'
import { extractUploadHeaders } from '../utils/headers'
import { safeAxios } from '../utils/safeAxios'

const socEndpoint = '/soc'

/**
* Upload single owner chunk (SOC) to a Bee node
*
* @param url Bee URL
* @param owner Owner's ethereum address in hex
* @param identifier Arbitrary identifier in hex
* @param signature Signature in hex
* @param data Content addressed chunk data to be uploaded
* @param options Aditional options like tag, encryption, pinning
*/
export async function upload(
url: string,
owner: string,
identifier: string,
signature: string,
data: Uint8Array,
options?: UploadOptions,
): Promise<ReferenceResponse> {
const response = await safeAxios<ReferenceResponse>({
method: 'post',
url: `${url}${socEndpoint}/${owner}/${identifier}`,
data,
headers: {
'content-type': 'application/octet-stream',
...extractUploadHeaders(options),
},
responseType: 'json',
params: { sig: signature },
})

return response.data
}
13 changes: 6 additions & 7 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,14 @@ export interface DownloadOptions {
export interface UploadHeaders {
'swarm-pin'?: string
'swarm-encrypt'?: string
'swarm-tag-uid'?: string
'swarm-tag'?: string
}

export interface Tag {
total: number
split: number
seen: number
stored: number
sent: number
processed: number
synced: number
uid: number
name: string
address: string
startedAt: string
}

Expand Down Expand Up @@ -94,6 +89,10 @@ export interface BeeResponse {
code: number
}

export interface ReferenceResponse {
reference: string
}

/**
* These type are used to create new nominal types
*
Expand Down
2 changes: 1 addition & 1 deletion src/utils/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function extractUploadHeaders(options?: UploadOptions): UploadHeaders {

if (options?.encrypt) headers['swarm-encrypt'] = String(options.encrypt)

if (options?.tag) headers['swarm-tag-uid'] = String(options.tag)
if (options?.tag) headers['swarm-tag'] = String(options.tag)

return headers
}
14 changes: 7 additions & 7 deletions test/chunk/bmt.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { bmtHash } from '../../src/chunk/bmt'
import { beeUrl, okResponse, randomByteArray } from '../utils'
import { beeUrl, randomByteArray } from '../utils'
import * as chunk from '../../src/modules/chunk'
import { makeSpan } from '../../src/chunk/span'
import { bytesToHex } from '../../src/utils/hex'
Expand All @@ -26,9 +26,9 @@ describe('bmt', () => {
const span = makeSpan(i)
const data = new Uint8Array([...span, ...payload])

const hash = bytesToHex(bmtHash(data))
const response = await chunk.upload(beeUrl(), hash, data)
expect(response).toEqual(okResponse)
const reference = bytesToHex(bmtHash(data))
const response = await chunk.upload(beeUrl(), data)
expect(response).toEqual({ reference })
}
})

Expand All @@ -37,8 +37,8 @@ describe('bmt', () => {
const span = makeSpan(payload.length)
const data = new Uint8Array([...span, ...payload])

const hash = bytesToHex(bmtHash(data))
const response = await chunk.upload(beeUrl(), hash, data)
expect(response).toEqual(okResponse)
const reference = bytesToHex(bmtHash(data))
const response = await chunk.upload(beeUrl(), data)
expect(response).toEqual({ reference })
})
})
26 changes: 17 additions & 9 deletions test/chunk/soc.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Bytes, verifyBytes } from '../../src/utils/bytes'
import { makeSingleOwnerChunk, verifySingleOwnerChunk } from '../../src/chunk/soc'
import { makeSingleOwnerChunk, verifySingleOwnerChunk, uploadSingleOwnerChunk } from '../../src/chunk/soc'
import { makeContentAddressedChunk, verifyChunk } from '../../src/chunk/cac'
import { beeUrl, okResponse, testIdentity } from '../utils'
import { beeUrl, testIdentity } from '../utils'
import { makeDefaultSigner } from '../../src/chunk/signer'
import { uploadChunk } from '../../src/chunk/upload'
import { serializeBytes } from '../../src/chunk/serialize'
import { makeSpan } from '../../src/chunk/span'
import * as chunkAPI from '../../src/modules/chunk'
Expand Down Expand Up @@ -36,10 +35,10 @@ describe('soc', () => {
test('upload content address chunk', async () => {
const cac = makeContentAddressedChunk(payload)
const address = cac.address()
const hash = bytesToHex(address)
const response = await chunkAPI.upload(beeUrl(), hash, cac.data)
const reference = bytesToHex(address)
const response = await chunkAPI.upload(beeUrl(), cac.data)

expect(response).toEqual(okResponse)
expect(response).toEqual({ reference })
})

test('download content address chunk', async () => {
Expand All @@ -51,15 +50,24 @@ describe('soc', () => {
expect(chunkAddress).toEqual(address)
})

test('upload single owner chunk', async () => {
test('single owner chunk creation', async () => {
const cac = makeContentAddressedChunk(payload)
const soc = await makeSingleOwnerChunk(cac, identifier, signer)
const socAddress = bytesToHex(soc.address())
const owner = soc.owner()

expect(socAddress).toEqual(socHash)
expect(owner).toEqual(signer.address)
})

test('upload single owner chunk', async () => {
const cac = makeContentAddressedChunk(payload)
const soc = await makeSingleOwnerChunk(cac, identifier, signer)
const socAddress = bytesToHex(soc.address())

const response = await uploadChunk(beeUrl(), soc)
const response = await uploadSingleOwnerChunk(beeUrl(), soc)

expect(response).toEqual(okResponse)
expect(response).toEqual({ reference: socAddress })
})

test('download single owner chunk', async () => {
Expand Down
12 changes: 6 additions & 6 deletions test/modules/chunk.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as chunk from '../../src/modules/chunk'
import { beeUrl, invalidReference, okResponse } from '../utils'
import { beeUrl, invalidReference } from '../utils'

const BEE_URL = beeUrl()

Expand All @@ -10,16 +10,16 @@ describe('modules/chunk', () => {
const span = new Uint8Array([payload.length, 0, 0, 0, 0, 0, 0, 0])
const data = new Uint8Array([...span, ...payload])
// the hash is hardcoded because we would need the bmt hasher otherwise
const hash = 'ca6357a08e317d15ec560fef34e4c45f8f19f01c372aa70f1da72bfa7f1a4338'
const reference = 'ca6357a08e317d15ec560fef34e4c45f8f19f01c372aa70f1da72bfa7f1a4338'

const response = await chunk.upload(BEE_URL, hash, data)
expect(response).toEqual(okResponse)
const response = await chunk.upload(BEE_URL, data)
expect(response).toEqual({ reference })

const downloadedData = await chunk.download(BEE_URL, hash)
const downloadedData = await chunk.download(BEE_URL, response.reference)
expect(downloadedData).toEqual(data)
})

it('should catch error', async () => {
await expect(chunk.download(BEE_URL, invalidReference)).rejects.toThrow('Internal Server Error')
await expect(chunk.download(BEE_URL, invalidReference)).rejects.toThrow('Not Found')
})
})
Loading

0 comments on commit 5eb1d15

Please sign in to comment.