Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions src/mantaray/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ function setBit(bytes: Uint8Array, index: number): void {
bytes[byteIndex] = bytes[byteIndex]! | (1 << bitIndex)
}

// Random per node, so structurally identical nodes get distinct addresses instead of colliding in one postage bucket.
function randomObfuscationKey(): Uint8Array {
const key = new Uint8Array(32)
crypto.getRandomValues(key)

return key
}

function getBit(bytes: Uint8Array, index: number): boolean {
const byteIndex = Math.floor(index / 8)
const bitIndex = index % 8
Expand All @@ -50,7 +58,7 @@ interface MantarayNodeOptions {
* single-byte-keyed edges to child nodes, each carrying a shared path prefix.
*/
export class MantarayNode {
public obfuscationKey: Uint8Array = new Uint8Array(32)
public obfuscationKey: Uint8Array
public selfAddress: Uint8Array | null = null
public targetAddress: Uint8Array = new Uint8Array(32)
public metadata: Record<string, string> | undefined | null = null
Expand Down Expand Up @@ -79,9 +87,7 @@ export class MantarayNode {
this.metadata = options.metadata
}

if (options?.obfuscationKey) {
this.obfuscationKey = options.obfuscationKey
}
this.obfuscationKey = options?.obfuscationKey ?? randomObfuscationKey()

if (options?.path) {
this.path = options.path
Expand Down Expand Up @@ -119,17 +125,10 @@ export class MantarayNode {
}
}

if (this.encrypt && equals(this.obfuscationKey, new Uint8Array(32))) {
this.obfuscationKey = new Uint8Array(32)
crypto.getRandomValues(this.obfuscationKey)
}

// A null (all-zero) targetAddress means this node has no entry of its
// own (e.g. a metadata-only "/" node). Bee (Go) then writes a 0-byte
// entry, inferring refBytesSize from a fork's reference width when one
// exists, rather than padding the entry out to the default 32/64 bytes -
// matching that here keeps hashes identical to a real Bee-produced
// manifest for the same content.
// exists, rather than padding the entry out to the default 32/64 bytes.
const hasEntry = !equals(this.targetAddress, new Uint8Array(this.targetAddress.length))
let refBytesSize = 0

Expand Down
24 changes: 22 additions & 2 deletions test/mantaray.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,33 @@ describe('MantarayNode marshal/unmarshal round-trip', () => {
})

describe('MantarayNode.calculateSelfAddress / saveRecursively', () => {
it('calculateSelfAddress is deterministic for identical trees', async () => {
it('calculateSelfAddress differs for identical trees, from their random obfuscation keys', async () => {
const a = new MantarayNode()
a.addFork('foo', arbitraryReference())
const b = new MantarayNode()
b.addFork('foo', arbitraryReference())

expect((await a.calculateSelfAddress()).toHex()).toBe((await b.calculateSelfAddress()).toHex())
expect((await a.calculateSelfAddress()).toHex()).not.toBe((await b.calculateSelfAddress()).toHex())
})

it('calculateSelfAddress is stable across calls on one tree', async () => {
const node = new MantarayNode()
node.addFork('foo', arbitraryReference())

expect((await node.calculateSelfAddress()).toHex()).toBe((await node.calculateSelfAddress()).toHex())
})

it('calculateSelfAddress is deterministic when every node key is given', async () => {
const key = new Uint8Array(32).fill(7)
const build = () => {
const root = new MantarayNode({ obfuscationKey: key })
root.addFork('foo', arbitraryReference())
root.find('foo')!.obfuscationKey = key

return root
}

expect((await build().calculateSelfAddress()).toHex()).toBe((await build().calculateSelfAddress()).toHex())
})

it('calculateSelfAddress throws for an encrypted node (use saveRecursively instead)', async () => {
Expand Down
39 changes: 39 additions & 0 deletions test/regression/mantaray-obfuscation-key.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, it } from 'vitest'
import { equals } from '../../src/bytes/encoding.js'
import { MantarayNode } from '../../src/mantaray/node.js'

// Regression: https://github.com/ethersphere/core-sdk/issues/8
const INDEX_METADATA = { 'website-index-document': 'index.html' }

function manifest(fileReference: number): MantarayNode {
const root = new MantarayNode()
root.addFork('/', new Uint8Array(32), INDEX_METADATA)
root.addFork('index.html', new Uint8Array(32).fill(fileReference))

return root
}

describe('MantarayNode obfuscation key', () => {
it('is random for a fresh node', () => {
const node = new MantarayNode()

expect(equals(node.obfuscationKey, new Uint8Array(32))).toBe(false)
expect(equals(node.obfuscationKey, new MantarayNode().obfuscationKey)).toBe(false)
})

it('gives the metadata-only "/" node a distinct address per manifest', async () => {
const a = await manifest(1).find('/')!.calculateSelfAddress()
const b = await manifest(2).find('/')!.calculateSelfAddress()

expect(a.toHex()).not.toBe(b.toHex())
})

it('is stored in the clear, so a randomized node still round-trips', async () => {
const node = manifest(3)
const restored = MantarayNode.unmarshalFromData(await node.marshal())

expect(restored.obfuscationKey).toEqual(node.obfuscationKey)
expect(restored.forks.size).toBe(node.forks.size)
expect(await restored.marshal()).toEqual(await node.marshal())
})
})
Loading