/**
* StorageProofService (Tier-2 relay side) — in-process tests.
*
* The provider runs buildStorageProof for REAL against a temp-dir Hypercore
* wrapped in a mock node whose appRegistry returns a fake seeded entry. We then
* verify the produced proof with verifyStorageProof, and assert the guards
* (phantom-core, bad input, out-of-range, rate-limit) all fire.
*/
import test from 'brittle'
import Hypercore from 'hypercore'
import b4a from 'b4a'
import sodium from 'sodium-universal'
import os from 'os'
import path from 'path'
import { StorageProofService } from 'p2p-hiveservices/builtin/storage-proof-service.js'
import {
PROOF_KIND_RETRIEVABILITY,
RETRIEVABILITY_PROOF_SIGNATURE_PROFILE,
STORAGE_PROOF_LEGACY_SIGNATURE_PROFILE,
verifyStorageProof
} from 'p2p-hiverelay/core/protocol/proof-of-storage.js'
let _n = 0
const tmp = () => path.join(os.tmpdir(), 'hr-sp-' + process.pid + '-' + (_n++))
function relayKeyPair () {
const publicKey = b4a.alloc(sodium.crypto_sign_PUBLICKEYBYTES)
const secretKey = b4a.alloc(sodium.crypto_sign_SECRETKEYBYTES)
sodium.crypto_sign_keypair(publicKey, secretKey)
return { publicKey, secretKey }
}
const freshNonce = () => { const n = b4a.alloc(32); sodium.randombytes_buf(n); return n }
const hexNonce = () => b4a.toString(freshNonce(), 'hex')
async function seededCore (blocks) {
const core = new Hypercore(tmp())
await core.ready()
await core.append(blocks)
return core
}
// Mock node: appRegistry.has/get return a fake entry whose drive.core is the
// real Hypercore; keyPair is the relay's signing identity (provider start ctx).
// _shouldRedactEntry mirrors the real AppRegistry predicate so the privacy gate
// is exercised against the same logic the catalog uses.
function mockNode (core, keyPair, opts = {}) {
const keyHex = b4a.toString(core.key, 'hex')
const entry = opts.noDrive
? {}
: { drive: { core, db: { core }, closed: false, closing: false }, blind: opts.blind === true, privacyTier: opts.privacyTier || 'public' }
return {
keyPair: opts.noKeyPair ? null : keyPair,
appRegistry: {
has: (k) => !opts.notSeeded && k === keyHex,
get: (k) => (k === keyHex ? entry : null),
_shouldRedactEntry: (e, o = {}) =>
e.blind === true || (o.redactPrivate === true && String(e.privacyTier || 'public').toLowerCase() !== 'public')
}
}
}
async function svcFor (core, keyPair, opts = {}) {
const svc = new StorageProofService(opts.ctor || {})
await svc.start({ node: mockNode(core, keyPair, opts) })
return svc
}
test('manifest labels storage-proof as proof-of-retrievability', (t) => {
const manifest = new StorageProofService().manifest()
t.is(manifest.name, 'storage-proof')
t.is(manifest.proofKind, PROOF_KIND_RETRIEVABILITY)
t.alike(manifest.signatureProfiles, [
STORAGE_PROOF_LEGACY_SIGNATURE_PROFILE,
RETRIEVABILITY_PROOF_SIGNATURE_PROFILE
])
t.is(manifest.preferredSignatureProfile, RETRIEVABILITY_PROOF_SIGNATURE_PROFILE)
t.ok(/retrievability/.test(manifest.description))
})
test('prove: valid proof verifies against the drive key', async (t) => {
const core = await seededCore([b4a.from('a'), b4a.from('bb'), b4a.from('ccc')])
const relay = relayKeyPair()
const svc = await svcFor(core, relay)
const nonce = freshNonce()
const resp = await svc.prove(
{ coreKey: b4a.toString(core.key, 'hex'), index: 2, nonce: b4a.toString(nonce, 'hex') },
{ remotePubkey: 'cafe'.repeat(16) }
)
t.is(resp.relayPubkey, b4a.toString(relay.publicKey, 'hex'), 'signed by the relay identity')
const verifier = new Hypercore(tmp(), core.key)
await verifier.ready()
const v = await verifyStorageProof({
verifierCore: verifier,
response: resp,
expect: { driveKey: core.key, index: 2, nonce, relayPubkey: relay.publicKey, minLength: core.length }
})
t.ok(v.valid, 'proof valid'); t.is(v.reason, null)
await verifier.close(); await core.close()
})
test('prove: NOT_SEEDED for an unknown key (phantom-core guard — never store.get)', async (t) => {
const core = await seededCore([b4a.from('x')])
const svc = await svcFor(core, relayKeyPair(), { notSeeded: true })
await t.exception(svc.prove(
{ coreKey: b4a.toString(core.key, 'hex'), index: 0, nonce: hexNonce() },
{ remotePubkey: 'ab'.repeat(32) }
), /NOT_SEEDED/)
await core.close()
})
test('prove: seeded entry without an open drive (placeholder) => NOT_SEEDED', async (t) => {
const core = await seededCore([b4a.from('x')])
const svc = await svcFor(core, relayKeyPair(), { noDrive: true })
await t.exception(svc.prove(
{ coreKey: b4a.toString(core.key, 'hex'), index: 0, nonce: hexNonce() }, {}
), /NOT_SEEDED/)
await core.close()
})
test('prove: malformed inputs rejected before any core access', async (t) => {
const svc = new StorageProofService()
await svc.start({
node: {
keyPair: relayKeyPair(),
appRegistry: {
has () { throw new Error('REGISTRY_TOUCHED') },
get () { throw new Error('REGISTRY_TOUCHED') },
_shouldRedactEntry () { throw new Error('REGISTRY_TOUCHED') }
}
}
})
const k = 'aa'.repeat(32)
await t.exception(svc.prove({ coreKey: 'zz', index: 0, nonce: hexNonce() }, {}), /BAD_CORE_KEY/)
await t.exception(svc.prove({ coreKey: k, index: -1, nonce: hexNonce() }, {}), /BAD_INDEX/)
await t.exception(svc.prove({ coreKey: k, index: 2 ** 32, nonce: hexNonce() }, {}), /BAD_INDEX/)
await t.exception(svc.prove({ coreKey: k, index: 1.25, nonce: hexNonce() }, {}), /BAD_INDEX/)
await t.exception(svc.prove({ coreKey: k, index: 0, nonce: 'nothex' }, {}), /BAD_NONCE/)
await t.exception(svc.prove({
coreKey: k,
index: 0,
nonce: hexNonce(),
signatureProfile: 'unknown-proof-profile'
}, {}), /UNSUPPORTED_SIGNATURE_PROFILE/)
await svc.stop()
})
test('prove: BLOCK_OUT_OF_RANGE surfaced honestly from the helper', async (t) => {
const core = await seededCore([b4a.from('only')])
const svc = await svcFor(core, relayKeyPair())
await t.exception(svc.prove(
{ coreKey: b4a.toString(core.key, 'hex'), index: 99, nonce: hexNonce() }, {}
), /BLOCK_OUT_OF_RANGE/)
await core.close()
})
test('prove: rate limit trips after the burst for a single caller', async (t) => {
const core = await seededCore([b4a.from('a'), b4a.from('b')])
const svc = await svcFor(core, relayKeyPair(), { ctor: { proofsPerMin: 0, proofBurst: 1 } })
const caller = { remotePubkey: 'ff'.repeat(32) }
const k = b4a.toString(core.key, 'hex')
await svc.prove({ coreKey: k, index: 0, nonce: hexNonce() }, caller) // 1st ok
await t.exception(svc.prove({ coreKey: k, index: 0, nonce: hexNonce() }, caller), /RATE_LIMITED/)
await core.close()
})
test('prove: NO_RELAY_KEYPAIR when the node has no identity key', async (t) => {
const core = await seededCore([b4a.from('a')])
const svc = await svcFor(core, relayKeyPair(), { noKeyPair: true })
await t.exception(svc.prove(
{ coreKey: b4a.toString(core.key, 'hex'), index: 0, nonce: hexNonce() }, {}
), /NO_RELAY_KEYPAIR/)
await core.close()
})
test('prove: BLIND drive => NOT_SEEDED (privacy gate, indistinguishable from not-held)', async (t) => {
const core = await seededCore([b4a.from('secret-0'), b4a.from('secret-1')])
const svc = await svcFor(core, relayKeyPair(), { blind: true })
await t.exception(svc.prove(
{ coreKey: b4a.toString(core.key, 'hex'), index: 0, nonce: hexNonce() },
{ remotePubkey: 'snoop'.repeat(12).slice(0, 64) }
), /NOT_SEEDED/, 'a blind drive must not be confirmable via a signed proof')
await core.close()
})
test('prove: non-public privacy tier => NOT_SEEDED (redactPrivate gate)', async (t) => {
const core = await seededCore([b4a.from('a'), b4a.from('b')])
const svc = await svcFor(core, relayKeyPair(), { privacyTier: 'private' })
await t.exception(svc.prove(
{ coreKey: b4a.toString(core.key, 'hex'), index: 0, nonce: hexNonce() }, {}
), /NOT_SEEDED/)
await core.close()
})
test('prove: GLOBAL cap bounds total proof work across DISTINCT identities', async (t) => {
const core = await seededCore([b4a.from('a'), b4a.from('b')])
// Global burst 1, no refill — sybil identities can't bypass it.
const svc = await svcFor(core, relayKeyPair(), { ctor: { globalProofBurst: 1, globalProofsPerMin: 0 } })
const k = b4a.toString(core.key, 'hex')
await svc.prove({ coreKey: k, index: 0, nonce: hexNonce() }, { remotePubkey: 'aa'.repeat(32) }) // 1st identity ok
await t.exception(
svc.prove({ coreKey: k, index: 1, nonce: hexNonce() }, { remotePubkey: 'bb'.repeat(32) }), // different identity
/RATE_LIMITED/, 'fresh identity cannot bypass the global proof budget'
)
await core.close()
})
test('prove: cheap rejects (not-seeded) do NOT spend the global budget', async (t) => {
const core = await seededCore([b4a.from('a')])
const svc = await svcFor(core, relayKeyPair(), { ctor: { globalProofBurst: 1, globalProofsPerMin: 0 } })
const k = b4a.toString(core.key, 'hex')
// Flood of not-seeded requests must not drain the budget...
for (let i = 0; i < 5; i++) {
await t.exception(svc.prove({ coreKey: 'cc'.repeat(32), index: 0, nonce: hexNonce() }, {}), /NOT_SEEDED/)
}
// ...so a genuine proof still succeeds afterwards.
const resp = await svc.prove({ coreKey: k, index: 0, nonce: hexNonce() }, { remotePubkey: 'dd'.repeat(32) })
t.ok(resp && resp.signature, 'honest proof still served after a not-seeded flood')
await core.close()
})
test('_buckets stays bounded under many distinct caller identities', async (t) => {
const core = await seededCore([b4a.from('a')])
const svc = await svcFor(core, relayKeyPair(), { ctor: { maxBuckets: 3 } })
const k = b4a.toString(core.key, 'hex')
for (let i = 0; i < 8; i++) {
await svc.prove({ coreKey: k, index: 0, nonce: hexNonce() }, { remotePubkey: String(i).padStart(64, '0') })
}
t.ok(svc._buckets.size <= 3, 'force-eviction keeps _buckets at the cap (' + svc._buckets.size + ')')
await svc.stop()
t.is(svc._buckets.size, 0, 'stop() clears buckets')
await core.close()
})