@provenonce/beats-client 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +118 -1
- package/index.mjs +196 -9
- package/package.json +1 -1
package/index.d.ts
CHANGED
|
@@ -117,10 +117,78 @@ export interface VerifyApiResponse {
|
|
|
117
117
|
[key: string]: unknown;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
export interface BeatObject {
|
|
121
|
+
index: number;
|
|
122
|
+
hash: string;
|
|
123
|
+
prev: string;
|
|
124
|
+
timestamp: number;
|
|
125
|
+
nonce?: string;
|
|
126
|
+
anchor_hash?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface SpotCheck {
|
|
130
|
+
index: number;
|
|
131
|
+
hash: string;
|
|
132
|
+
prev: string;
|
|
133
|
+
nonce?: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface WorkProofRequest {
|
|
137
|
+
from_beat: number;
|
|
138
|
+
to_beat: number;
|
|
139
|
+
from_hash: string;
|
|
140
|
+
to_hash: string;
|
|
141
|
+
beats_computed: number;
|
|
142
|
+
difficulty: number;
|
|
143
|
+
anchor_index: number;
|
|
144
|
+
anchor_hash?: string;
|
|
145
|
+
spot_checks: SpotCheck[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface WorkProofReceiptPayload {
|
|
149
|
+
type: 'work_proof';
|
|
150
|
+
beats_verified: number;
|
|
151
|
+
difficulty: number;
|
|
152
|
+
anchor_index: number;
|
|
153
|
+
anchor_hash: string | null;
|
|
154
|
+
utc: number;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface WorkProofResponse {
|
|
158
|
+
ok: boolean;
|
|
159
|
+
valid: boolean;
|
|
160
|
+
receipt?: WorkProofReceiptPayload;
|
|
161
|
+
signature?: string;
|
|
162
|
+
public_key?: string;
|
|
163
|
+
spot_checks_verified?: number;
|
|
164
|
+
reason?: string;
|
|
165
|
+
failed_indices?: number[];
|
|
166
|
+
_note?: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface KeyInfo {
|
|
170
|
+
public_key_base58: string;
|
|
171
|
+
public_key_hex: string;
|
|
172
|
+
signing_context: string;
|
|
173
|
+
purpose: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface KeyResponse {
|
|
177
|
+
/** Timestamp receipt key (backward compat) */
|
|
178
|
+
public_key_base58: string;
|
|
179
|
+
public_key_hex: string;
|
|
180
|
+
algorithm: string;
|
|
181
|
+
keys: {
|
|
182
|
+
timestamp: KeyInfo;
|
|
183
|
+
work_proof: KeyInfo;
|
|
184
|
+
};
|
|
185
|
+
_note?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
120
188
|
export interface BeatsClient {
|
|
121
189
|
getHealth(): Promise<HealthResponse>;
|
|
122
190
|
getAnchor(opts?: AnchorOptions): Promise<AnchorResponse>;
|
|
123
|
-
getKey(): Promise<
|
|
191
|
+
getKey(): Promise<KeyResponse>;
|
|
124
192
|
verify(payload: unknown): Promise<VerifyApiResponse>;
|
|
125
193
|
timestampHash(hash: string): Promise<TimestampResponse>;
|
|
126
194
|
|
|
@@ -143,8 +211,57 @@ export interface BeatsClient {
|
|
|
143
211
|
/** Returns true if chain continuity is broken and resync() is required. */
|
|
144
212
|
isBroken(): boolean;
|
|
145
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Submit a work proof to the Beats service and receive a signed receipt.
|
|
216
|
+
* The receipt certifies N beats at difficulty D anchored to a global beat.
|
|
217
|
+
* Policy-free: the caller (Registry or any consumer) decides what N means.
|
|
218
|
+
*/
|
|
219
|
+
submitWorkProof(proof: WorkProofRequest): Promise<WorkProofResponse>;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Verify a work-proof receipt signature offline.
|
|
223
|
+
* Uses the work_proof key from GET /api/v1/beat/key (distinct from timestamp key).
|
|
224
|
+
*/
|
|
225
|
+
verifyWorkProofReceipt(
|
|
226
|
+
receiptResponse: WorkProofResponse,
|
|
227
|
+
opts?: { publicKey?: string },
|
|
228
|
+
): Promise<boolean>;
|
|
229
|
+
|
|
146
230
|
/** Internal: resolve public key from cache or auto-fetch. */
|
|
147
231
|
_resolveKey(): Promise<string | null>;
|
|
148
232
|
}
|
|
149
233
|
|
|
150
234
|
export declare function createBeatsClient(options?: BeatsClientOptions): BeatsClient;
|
|
235
|
+
|
|
236
|
+
// ============ STANDALONE COMPUTE (Node.js only) ============
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Compute a single beat — sequential SHA-256 hash chain.
|
|
240
|
+
* Node.js only (uses node:crypto). Not browser-compatible.
|
|
241
|
+
*
|
|
242
|
+
* @param prevHash Previous beat hash (64 hex)
|
|
243
|
+
* @param beatIndex Beat index (monotonically increasing)
|
|
244
|
+
* @param difficulty Hash iterations per beat (default 1000)
|
|
245
|
+
* @param nonce Optional entropy
|
|
246
|
+
* @param anchorHash Optional global anchor hash to weave in
|
|
247
|
+
*/
|
|
248
|
+
export declare function computeBeat(
|
|
249
|
+
prevHash: string,
|
|
250
|
+
beatIndex: number,
|
|
251
|
+
difficulty?: number,
|
|
252
|
+
nonce?: string,
|
|
253
|
+
anchorHash?: string,
|
|
254
|
+
): Promise<BeatObject>;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Compute the genesis beat for a local chain.
|
|
258
|
+
* Deterministic from caller-provided seed + domain prefix.
|
|
259
|
+
* Node.js only.
|
|
260
|
+
*
|
|
261
|
+
* @param seed Unique identifier (e.g. agent hash)
|
|
262
|
+
* @param domainPrefix Namespace prefix (default: 'beats:genesis:v1:')
|
|
263
|
+
*/
|
|
264
|
+
export declare function createGenesisBeat(
|
|
265
|
+
seed: string,
|
|
266
|
+
domainPrefix?: string,
|
|
267
|
+
): Promise<BeatObject>;
|
package/index.mjs
CHANGED
|
@@ -77,12 +77,33 @@ async function verifyEd25519(payload, signatureBase64, publicKey) {
|
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
79
|
* Recompute an anchor's hash from its fields (B-3).
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* A-4: If solana_entropy is present, uses the v2 domain-prefixed nonce.
|
|
80
|
+
* V3: If solana_entropy is present, uses binary-canonical single SHA-256.
|
|
81
|
+
* V1 legacy: string-based hash with difficulty iteration (Node.js only).
|
|
84
82
|
*/
|
|
85
|
-
const ANCHOR_DOMAIN_PREFIX = '
|
|
83
|
+
const ANCHOR_DOMAIN_PREFIX = 'PROVENONCE_BEATS_V1';
|
|
84
|
+
|
|
85
|
+
function hexToUint8Array(hex) {
|
|
86
|
+
const arr = new Uint8Array(hex.length / 2);
|
|
87
|
+
for (let i = 0; i < arr.length; i++) {
|
|
88
|
+
arr[i] = parseInt(hex.substr(i * 2, 2), 16);
|
|
89
|
+
}
|
|
90
|
+
return arr;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function u64beBytes(n) {
|
|
94
|
+
const buf = new Uint8Array(8);
|
|
95
|
+
const hi = Math.floor(n / 0x100000000);
|
|
96
|
+
const lo = n >>> 0;
|
|
97
|
+
buf[0] = (hi >>> 24) & 0xff;
|
|
98
|
+
buf[1] = (hi >>> 16) & 0xff;
|
|
99
|
+
buf[2] = (hi >>> 8) & 0xff;
|
|
100
|
+
buf[3] = hi & 0xff;
|
|
101
|
+
buf[4] = (lo >>> 24) & 0xff;
|
|
102
|
+
buf[5] = (lo >>> 16) & 0xff;
|
|
103
|
+
buf[6] = (lo >>> 8) & 0xff;
|
|
104
|
+
buf[7] = lo & 0xff;
|
|
105
|
+
return buf;
|
|
106
|
+
}
|
|
86
107
|
|
|
87
108
|
async function recomputeAnchorHash(anchor) {
|
|
88
109
|
if (!anchor || !HEX64.test(anchor.hash) || !HEX64.test(anchor.prev_hash)) return false;
|
|
@@ -91,11 +112,41 @@ async function recomputeAnchorHash(anchor) {
|
|
|
91
112
|
if (!Number.isInteger(anchor.utc) || anchor.utc < 0) return false;
|
|
92
113
|
if (!Number.isInteger(anchor.epoch) || anchor.epoch < 0) return false;
|
|
93
114
|
|
|
115
|
+
if (anchor.solana_entropy) {
|
|
116
|
+
// V3: binary-canonical single SHA-256
|
|
117
|
+
const prefix = new TextEncoder().encode(ANCHOR_DOMAIN_PREFIX); // 19 bytes
|
|
118
|
+
const prev = hexToUint8Array(anchor.prev_hash); // 32 bytes
|
|
119
|
+
const idx = u64beBytes(anchor.beat_index); // 8 bytes
|
|
120
|
+
const entropy = decodeBase58(anchor.solana_entropy); // 32 bytes
|
|
121
|
+
|
|
122
|
+
const preimage = new Uint8Array(prefix.length + prev.length + idx.length + entropy.length);
|
|
123
|
+
preimage.set(prefix, 0);
|
|
124
|
+
preimage.set(prev, prefix.length);
|
|
125
|
+
preimage.set(idx, prefix.length + prev.length);
|
|
126
|
+
preimage.set(entropy, prefix.length + prev.length + idx.length);
|
|
127
|
+
|
|
128
|
+
// Web Crypto path
|
|
129
|
+
const subtle = globalThis.crypto?.subtle;
|
|
130
|
+
if (subtle) {
|
|
131
|
+
try {
|
|
132
|
+
const digest = await subtle.digest('SHA-256', preimage);
|
|
133
|
+
const hashArr = Array.from(new Uint8Array(digest));
|
|
134
|
+
const computed = hashArr.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
135
|
+
return computed === anchor.hash;
|
|
136
|
+
} catch {
|
|
137
|
+
// Fall through to Node.js
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Node.js fallback
|
|
142
|
+
const { createHash } = await import('node:crypto');
|
|
143
|
+
const computed = createHash('sha256').update(preimage).digest('hex');
|
|
144
|
+
return computed === anchor.hash;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// V1 legacy: string-based hash with difficulty iteration
|
|
94
148
|
const { createHash } = await import('node:crypto');
|
|
95
|
-
|
|
96
|
-
const nonce = anchor.solana_entropy
|
|
97
|
-
? `${ANCHOR_DOMAIN_PREFIX}:${anchor.utc}:${anchor.epoch}:${anchor.solana_entropy}`
|
|
98
|
-
: `anchor:${anchor.utc}:${anchor.epoch}`;
|
|
149
|
+
const nonce = `anchor:${anchor.utc}:${anchor.epoch}`;
|
|
99
150
|
const seed = `${anchor.prev_hash}:${anchor.beat_index}:${nonce}`;
|
|
100
151
|
let current = createHash('sha256').update(seed, 'utf8').digest('hex');
|
|
101
152
|
for (let i = 0; i < anchor.difficulty; i++) {
|
|
@@ -479,5 +530,141 @@ export function createBeatsClient({
|
|
|
479
530
|
return null;
|
|
480
531
|
}
|
|
481
532
|
},
|
|
533
|
+
|
|
534
|
+
// ---- Work Proof ----
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Submit a work proof to the Beats service and receive a signed receipt.
|
|
538
|
+
*
|
|
539
|
+
* @param {object} proof
|
|
540
|
+
* @param {number} proof.from_beat Starting beat index
|
|
541
|
+
* @param {number} proof.to_beat Ending beat index
|
|
542
|
+
* @param {string} proof.from_hash Hash at from_beat (64 hex)
|
|
543
|
+
* @param {string} proof.to_hash Hash at to_beat (64 hex)
|
|
544
|
+
* @param {number} proof.beats_computed to_beat - from_beat
|
|
545
|
+
* @param {number} proof.difficulty Hash iterations per beat
|
|
546
|
+
* @param {number} proof.anchor_index Global anchor index woven in
|
|
547
|
+
* @param {string} [proof.anchor_hash] Anchor hash woven in (64 hex)
|
|
548
|
+
* @param {Array} proof.spot_checks Spot-checked beats for verification
|
|
549
|
+
* @param {number} .index Beat index
|
|
550
|
+
* @param {string} .hash Hash at this beat (64 hex)
|
|
551
|
+
* @param {string} .prev Previous hash (64 hex)
|
|
552
|
+
* @param {string} [.nonce] Optional nonce
|
|
553
|
+
*
|
|
554
|
+
* @returns {Promise<WorkProofResponse>}
|
|
555
|
+
*/
|
|
556
|
+
submitWorkProof(proof) {
|
|
557
|
+
return request('/api/v1/beat/work-proof', {
|
|
558
|
+
method: 'POST',
|
|
559
|
+
headers: { 'content-type': 'application/json' },
|
|
560
|
+
body: JSON.stringify(proof),
|
|
561
|
+
});
|
|
562
|
+
},
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Verify a work-proof receipt signature (offline).
|
|
566
|
+
* Uses the work_proof key from GET /api/v1/beat/key, not the timestamp key.
|
|
567
|
+
*
|
|
568
|
+
* @param {object} receiptResponse The full response from submitWorkProof()
|
|
569
|
+
* @param {object} [opts]
|
|
570
|
+
* @param {string} [opts.publicKey] Override: hex or base58 work-proof public key
|
|
571
|
+
*/
|
|
572
|
+
async verifyWorkProofReceipt(receiptResponse, opts = {}) {
|
|
573
|
+
const payload = receiptResponse?.receipt;
|
|
574
|
+
const signature = receiptResponse?.signature;
|
|
575
|
+
if (!payload || !signature) return false;
|
|
576
|
+
|
|
577
|
+
// Resolve work-proof public key: param > cached from /key endpoint
|
|
578
|
+
let key = opts.publicKey;
|
|
579
|
+
if (!key) {
|
|
580
|
+
try {
|
|
581
|
+
const keyData = await this.getKey();
|
|
582
|
+
key = keyData?.keys?.work_proof?.public_key_hex ||
|
|
583
|
+
keyData?.keys?.work_proof?.public_key_base58;
|
|
584
|
+
} catch {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (!key) return false;
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
return await verifyEd25519(payload, signature, key);
|
|
592
|
+
} catch {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ============ STANDALONE COMPUTE (Node.js only) ============
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Compute a single beat — sequential SHA-256 hash chain.
|
|
603
|
+
*
|
|
604
|
+
* Each beat requires `difficulty` sequential hash iterations.
|
|
605
|
+
* Because SHA-256 output feeds the next input, this cannot be
|
|
606
|
+
* parallelized. This is the CPU-work primitive for local beat chains.
|
|
607
|
+
*
|
|
608
|
+
* Node.js only: uses node:crypto. Not available in browser environments.
|
|
609
|
+
*
|
|
610
|
+
* @param {string} prevHash Previous beat hash (64 hex)
|
|
611
|
+
* @param {number} beatIndex Beat index (monotonically increasing)
|
|
612
|
+
* @param {number} [difficulty=1000] Hash iterations per beat
|
|
613
|
+
* @param {string} [nonce] Optional entropy
|
|
614
|
+
* @param {string} [anchorHash] Global anchor hash to weave in
|
|
615
|
+
* @returns {{ index, hash, prev, timestamp, nonce?, anchor_hash? }}
|
|
616
|
+
*/
|
|
617
|
+
export async function computeBeat(prevHash, beatIndex, difficulty = 1000, nonce, anchorHash) {
|
|
618
|
+
if (typeof prevHash !== 'string' || prevHash.length === 0) {
|
|
619
|
+
throw new Error('computeBeat: prevHash must be a non-empty string');
|
|
620
|
+
}
|
|
621
|
+
if (!Number.isInteger(beatIndex) || beatIndex < 0) {
|
|
622
|
+
throw new Error('computeBeat: beatIndex must be a non-negative integer');
|
|
623
|
+
}
|
|
624
|
+
const d = Math.max(1, Math.min(Number.isFinite(difficulty) ? Math.floor(difficulty) : 1000, 1_000_000));
|
|
625
|
+
|
|
626
|
+
const { createHash } = await import('node:crypto');
|
|
627
|
+
|
|
628
|
+
const seed = anchorHash
|
|
629
|
+
? `${prevHash}:${beatIndex}:${nonce || ''}:${anchorHash}`
|
|
630
|
+
: `${prevHash}:${beatIndex}:${nonce || ''}`;
|
|
631
|
+
|
|
632
|
+
let current = createHash('sha256').update(seed, 'utf8').digest('hex');
|
|
633
|
+
for (let i = 0; i < d; i++) {
|
|
634
|
+
current = createHash('sha256').update(current, 'utf8').digest('hex');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return {
|
|
638
|
+
index: beatIndex,
|
|
639
|
+
hash: current,
|
|
640
|
+
prev: prevHash,
|
|
641
|
+
timestamp: Date.now(),
|
|
642
|
+
nonce,
|
|
643
|
+
anchor_hash: anchorHash,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Compute the genesis beat for a local chain.
|
|
649
|
+
* Deterministic from caller-provided seed + optional domain prefix.
|
|
650
|
+
*
|
|
651
|
+
* Node.js only.
|
|
652
|
+
*
|
|
653
|
+
* @param {string} seed Unique identifier for this chain (e.g. agent hash)
|
|
654
|
+
* @param {string} [domainPrefix='beats:genesis:v1:'] Namespace prefix
|
|
655
|
+
*/
|
|
656
|
+
export async function createGenesisBeat(seed, domainPrefix = 'beats:genesis:v1:') {
|
|
657
|
+
if (!seed || typeof seed !== 'string') {
|
|
658
|
+
throw new Error('createGenesisBeat: seed must be a non-empty string');
|
|
659
|
+
}
|
|
660
|
+
const { createHash } = await import('node:crypto');
|
|
661
|
+
const genesisHash = createHash('sha256')
|
|
662
|
+
.update(`${domainPrefix}${seed}`, 'utf8')
|
|
663
|
+
.digest('hex');
|
|
664
|
+
return {
|
|
665
|
+
index: 0,
|
|
666
|
+
hash: genesisHash,
|
|
667
|
+
prev: '0'.repeat(64),
|
|
668
|
+
timestamp: Date.now(),
|
|
482
669
|
};
|
|
483
670
|
}
|