@provenonce/beats-client 0.3.0 → 0.5.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.
Files changed (3) hide show
  1. package/index.d.ts +2 -0
  2. package/index.mjs +60 -2
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -64,6 +64,8 @@ export interface BeatAnchor {
64
64
  difficulty: number;
65
65
  epoch: number;
66
66
  tx_signature: string;
67
+ /** A-4: Finalized Solana blockhash used as external entropy. Present on v2 anchors. */
68
+ solana_entropy?: string;
67
69
  }
68
70
 
69
71
  export interface ReceiptEnvelope {
package/index.mjs CHANGED
@@ -77,9 +77,34 @@ async function verifyEd25519(payload, signatureBase64, publicKey) {
77
77
 
78
78
  /**
79
79
  * Recompute an anchor's hash from its fields (B-3).
80
- * Requires Node.js crypto not available in browsers.
81
- * Returns true if the recomputed hash matches anchor.hash.
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).
82
82
  */
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
+ }
107
+
83
108
  async function recomputeAnchorHash(anchor) {
84
109
  if (!anchor || !HEX64.test(anchor.hash) || !HEX64.test(anchor.prev_hash)) return false;
85
110
  if (!Number.isInteger(anchor.beat_index) || anchor.beat_index < 0) return false;
@@ -87,6 +112,39 @@ async function recomputeAnchorHash(anchor) {
87
112
  if (!Number.isInteger(anchor.utc) || anchor.utc < 0) return false;
88
113
  if (!Number.isInteger(anchor.epoch) || anchor.epoch < 0) return false;
89
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
90
148
  const { createHash } = await import('node:crypto');
91
149
  const nonce = `anchor:${anchor.utc}:${anchor.epoch}`;
92
150
  const seed = `${anchor.prev_hash}:${anchor.beat_index}:${nonce}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@provenonce/beats-client",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Minimal client for the public Provenonce Beats service",
5
5
  "type": "module",
6
6
  "main": "index.mjs",