@provenonce/beats-client 0.4.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 (2) hide show
  1. package/index.mjs +60 -9
  2. package/package.json +1 -1
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
- * Requires Node.js crypto not available in browsers.
81
- * Returns true if the recomputed hash matches anchor.hash.
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 = 'provenonce:anchor:v2';
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
- // A-4: v2 nonce includes solana_entropy; legacy nonce for pre-A4 anchors
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++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@provenonce/beats-client",
3
- "version": "0.4.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",