|
| 1 | +// generate-fixtures.mjs |
| 2 | +// Run: node scripts/playground/generate-fixtures.mjs |
| 3 | +// Outputs hardcoded fixture values for scanner.js |
| 4 | +// |
| 5 | +// Uses the same algorithms as the Wraith SDK (inline, no SDK import) |
| 6 | +// so the docs repo doesn't need the SDK installed at build time. |
| 7 | + |
| 8 | +import { createHash } from 'node:crypto'; |
| 9 | + |
| 10 | +// ── Curve25519 field arithmetic ────────────────────────────── |
| 11 | +const P = (2n ** 255n) - 19n; |
| 12 | +const A24 = 121665n; |
| 13 | +const L = (2n ** 252n) + 27742317777372353535851937790883648493n; |
| 14 | + |
| 15 | +const fmod = x => { let r = x % P; return r < 0n ? r + P : r; }; |
| 16 | +const fadd = (a,b) => fmod(a+b); |
| 17 | +const fsub = (a,b) => fmod(a-b); |
| 18 | +const fmul = (a,b) => fmod(a*b); |
| 19 | +function fpow(base, exp) { |
| 20 | + base = fmod(base); let r = 1n; |
| 21 | + while (exp > 0n) { if (exp&1n) r=fmul(r,base); base=fmul(base,base); exp>>=1n; } |
| 22 | + return r; |
| 23 | +} |
| 24 | +const finv = a => fpow(a, P-2n); |
| 25 | + |
| 26 | +// ── helpers ────────────────────────────────────────────────── |
| 27 | +function bytesToBigintLE(b) { |
| 28 | + let n=0n; for (let i=b.length-1;i>=0;i--) n=(n<<8n)|BigInt(b[i]); return n; |
| 29 | +} |
| 30 | +function bigintToBytesLE(n, len) { |
| 31 | + const o=Buffer.alloc(len); for(let i=0;i<len;i++){o[i]=Number(n&0xffn);n>>=8n;} return o; |
| 32 | +} |
| 33 | +function hexToBytes(h) { return Buffer.from(h,'hex'); } |
| 34 | +function bytesToHex(b) { return Buffer.from(b).toString('hex'); } |
| 35 | +function concatBufs(...a) { return Buffer.concat(a.map(x=>Buffer.from(x))); } |
| 36 | +function sha256(...parts) { return createHash('sha256').update(concatBufs(...parts)).digest(); } |
| 37 | +function sha512(d) { return createHash('sha512').update(d).digest(); } |
| 38 | + |
| 39 | +// ── ed25519 math ───────────────────────────────────────────── |
| 40 | +const ED_D = fmod(-121665n * finv(121666n)); |
| 41 | +const ED_GY = fmod(4n * finv(5n)); |
| 42 | +const ED_GX = (() => { |
| 43 | + const y2=fmul(ED_GY,ED_GY), num=fsub(y2,1n), den=fadd(fmul(ED_D,y2),1n); |
| 44 | + const x2=fmul(num,finv(den)); |
| 45 | + let x=fpow(x2,(P+3n)/8n); |
| 46 | + if (fmul(x,x)!==x2) x=fmul(x,fpow(2n,(P-1n)/4n)); |
| 47 | + if (x%2n!==0n) x=P-x; return x; |
| 48 | +})(); |
| 49 | +const ED_G = [ED_GX, ED_GY, 1n, fmul(ED_GX,ED_GY)]; |
| 50 | + |
| 51 | +function edAdd(P1,P2){ |
| 52 | + const[X1,Y1,Z1,T1]=P1,[X2,Y2,Z2,T2]=P2; |
| 53 | + const A=fmul(fsub(Y1,X1),fsub(Y2,X2)),B=fmul(fadd(Y1,X1),fadd(Y2,X2)); |
| 54 | + const C=fmul(fmul(2n,ED_D),fmul(T1,T2)),D=fmul(2n,fmul(Z1,Z2)); |
| 55 | + const E=fsub(B,A),F=fsub(D,C),G=fadd(D,C),H=fadd(B,A); |
| 56 | + return[fmul(E,F),fmul(G,H),fmul(F,G),fmul(E,H)]; |
| 57 | +} |
| 58 | +function edMul(k,pt){ |
| 59 | + let Q=[0n,1n,1n,0n],R=pt; |
| 60 | + while(k>0n){if(k&1n)Q=edAdd(Q,R);R=edAdd(R,R);k>>=1n;} return Q; |
| 61 | +} |
| 62 | +function edCompress(pt){ |
| 63 | + const[X,Y,Z]=pt,zi=finv(Z),x=fmul(X,zi),y=fmul(Y,zi); |
| 64 | + const o=bigintToBytesLE(y,32); if(x&1n)o[31]|=0x80; return o; |
| 65 | +} |
| 66 | +function edDecompress(b){ |
| 67 | + const c=Buffer.from(b); const sign=(c[31]>>7)&1; c[31]&=0x7f; |
| 68 | + const y=bytesToBigintLE(c),y2=fmul(y,y),num=fsub(y2,1n),den=fadd(fmul(ED_D,y2),1n); |
| 69 | + const x2=fmul(num,finv(den)); let x=fpow(x2,(P+3n)/8n); |
| 70 | + if(fmul(x,x)!==fmod(x2))x=fmul(x,fpow(2n,(P-1n)/4n)); |
| 71 | + if(Number(x&1n)!==sign)x=P-x; return[x,y,1n,fmul(x,y)]; |
| 72 | +} |
| 73 | + |
| 74 | +// seedToScalar: expand 32-byte seed via SHA-512 lower half, clamp |
| 75 | +function seedToScalar(seed) { |
| 76 | + const h = sha512(seed); const a=h.slice(0,32); |
| 77 | + a[0]&=248; a[31]&=127; a[31]|=64; return bytesToBigintLE(a); |
| 78 | +} |
| 79 | +function seedToPubKey(seed) { return edCompress(edMul(seedToScalar(seed),ED_G)); } |
| 80 | + |
| 81 | +// ── X25519 ─────────────────────────────────────────────────── |
| 82 | +function clamp(s){ let c=s&((1n<<255n)-1n); c&=~7n; c|=(1n<<254n); return c; } |
| 83 | +function x25519(scalar,u){ |
| 84 | + const s=clamp(scalar); |
| 85 | + let x1=u,x2=1n,z2=0n,x3=u,z3=1n,sw=0n; |
| 86 | + for(let t=254n;t>=0n;t--){ |
| 87 | + const bit=(s>>t)&1n,xs=sw^bit; sw=bit; |
| 88 | + if(xs===1n){[x2,x3]=[x3,x2];[z2,z3]=[z3,z2];} |
| 89 | + const A=fadd(x2,z2),AA=fmul(A,A),B=fsub(x2,z2),BB=fmul(B,B),E=fsub(AA,BB); |
| 90 | + const C=fadd(x3,z3),D=fsub(x3,z3),DA=fmul(D,A),CB=fmul(C,B); |
| 91 | + x3=fpow(fadd(DA,CB),2n); z3=fmul(x1,fpow(fsub(DA,CB),2n)); |
| 92 | + x2=fmul(AA,BB); z2=fmul(E,fadd(AA,fmul(A24,E))); |
| 93 | + } |
| 94 | + if(sw===1n){[x2,x3]=[x3,x2];} |
| 95 | + return fmul(x2,finv(z2)); |
| 96 | +} |
| 97 | +function edPubToMont(b){ |
| 98 | + const c=Buffer.from(b); c[31]&=0x7f; |
| 99 | + const y=bytesToBigintLE(c),u=fmul(fadd(1n,y),finv(fsub(1n,y))); |
| 100 | + return bigintToBytesLE(u,32); |
| 101 | +} |
| 102 | +function edSeedToMontScalar(seed){ |
| 103 | + const h=sha512(seed),a=h.slice(0,32); a[0]&=248;a[31]&=127;a[31]|=64; return a; |
| 104 | +} |
| 105 | +function sharedSecret(viewingSeed, ephPub){ |
| 106 | + const priv=bytesToBigintLE(edSeedToMontScalar(viewingSeed)); |
| 107 | + const pubU=bytesToBigintLE(edPubToMont(ephPub)); |
| 108 | + return bigintToBytesLE(x25519(priv,pubU),32); |
| 109 | +} |
| 110 | + |
| 111 | +// ── Stellar StrKey ─────────────────────────────────────────── |
| 112 | +function crc16(data){ |
| 113 | + let c=0xffff; for(const b of data){c^=(b<<8);for(let i=0;i<8;i++)c=(c&0x8000)?((c<<1)^0x1021):(c<<1);c&=0xffff;} return c; |
| 114 | +} |
| 115 | +const B32='ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; |
| 116 | +function b32enc(bytes){ |
| 117 | + let bits=0,val=0,o=''; |
| 118 | + for(const b of bytes){val=(val<<8)|b;bits+=8;while(bits>=5){bits-=5;o+=B32[(val>>bits)&31];}} |
| 119 | + if(bits>0)o+=B32[(val<<(5-bits))&31]; return o; |
| 120 | +} |
| 121 | +function pubToAddr(pub){ |
| 122 | + const p=Buffer.alloc(33); p[0]=0x30; pub.copy(p,1); |
| 123 | + const c=crc16(p),f=Buffer.alloc(35); p.copy(f); f[33]=c&0xff;f[34]=(c>>8)&0xff; |
| 124 | + return b32enc(f); |
| 125 | +} |
| 126 | + |
| 127 | +// ── Fixture generation ─────────────────────────────────────── |
| 128 | +// Deterministic "demo" recipient keys |
| 129 | +const viewingSeed = sha256(Buffer.from('wraith:viewing:demo')); |
| 130 | +const spendingSeed = sha256(Buffer.from('wraith:spending:demo')); |
| 131 | +const viewingPub = seedToPubKey(viewingSeed); |
| 132 | +const spendingPub = seedToPubKey(spendingSeed); |
| 133 | +const spendingScalar = seedToScalar(spendingSeed); |
| 134 | + |
| 135 | +console.log('=== DEMO KEYS ==='); |
| 136 | +console.log('viewingKey (hex seed): ', bytesToHex(viewingSeed)); |
| 137 | +console.log('spendingScalar (bigint):', spendingScalar.toString()); |
| 138 | +console.log('spendingPubKey (hex): ', bytesToHex(spendingPub)); |
| 139 | +console.log('viewingPubKey (hex): ', bytesToHex(viewingPub)); |
| 140 | +console.log('metaAddress: ', `st:xlm:${bytesToHex(spendingPub)}${bytesToHex(viewingPub)}`); |
| 141 | +console.log(''); |
| 142 | + |
| 143 | +// Generate 5 fixture announcements; indices 0 and 2 are "yours" |
| 144 | +const YOURS_INDICES = new Set([0,2]); |
| 145 | + |
| 146 | +function makeAnnouncement(ephSeed, isYours) { |
| 147 | + // ephemeral keypair |
| 148 | + const ephPrivScalar = seedToScalar(ephSeed); |
| 149 | + const ephPub = edCompress(edMul(ephPrivScalar, ED_G)); |
| 150 | + |
| 151 | + if (!isYours) { |
| 152 | + // noise: random stealth address, wrong view tag |
| 153 | + const noiseKey = sha256(ephSeed, Buffer.from('noise')); |
| 154 | + const noisePub = edCompress(edMul(seedToScalar(noiseKey), ED_G)); |
| 155 | + const noiseAddr = pubToAddr(Buffer.from(noisePub)); |
| 156 | + const noiseTag = sha256(Buffer.from('wraith:tag:'), noiseKey)[0]; |
| 157 | + const meta = Buffer.alloc(32); meta[0] = noiseTag; |
| 158 | + return { |
| 159 | + schemeId: 1, |
| 160 | + stealthAddress: noiseAddr, |
| 161 | + caller: pubToAddr(Buffer.from(ephPub)), |
| 162 | + ephemeralPubKey: bytesToHex(ephPub), |
| 163 | + metadata: bytesToHex(meta), |
| 164 | + _yours: false, |
| 165 | + }; |
| 166 | + } |
| 167 | + |
| 168 | + // Compute shared secret from ephemeral privkey and recipient viewing pub |
| 169 | + const ephPriv = bytesToBigintLE(edSeedToMontScalar(ephSeed)); |
| 170 | + const viewingMontPub = bytesToBigintLE(edPubToMont(viewingPub)); |
| 171 | + const sharedU = x25519(ephPriv, viewingMontPub); |
| 172 | + const shared = bigintToBytesLE(sharedU, 32); |
| 173 | + |
| 174 | + // view tag |
| 175 | + const viewTag = sha256(Buffer.from('wraith:tag:'), shared)[0]; |
| 176 | + // hash scalar |
| 177 | + const hScalarBytes = sha256(Buffer.from('wraith:scalar:'), shared); |
| 178 | + const hScalar = bytesToBigintLE(hScalarBytes) % L; |
| 179 | + // stealth pub = spendingPub + hScalar * G |
| 180 | + const spendPt = edDecompress(spendingPub); |
| 181 | + const hG = edMul(hScalar, ED_G); |
| 182 | + const stealthPt = edAdd(spendPt, hG); |
| 183 | + const stealthPub = edCompress(stealthPt); |
| 184 | + const stealthAddr = pubToAddr(Buffer.from(stealthPub)); |
| 185 | + // metadata: first byte = view tag |
| 186 | + const meta = Buffer.alloc(32); meta[0] = viewTag; |
| 187 | + |
| 188 | + console.log(`[Match] ephSeed=${bytesToHex(ephSeed).slice(0,16)}…`); |
| 189 | + console.log(` ephPub: ${bytesToHex(ephPub)}`); |
| 190 | + console.log(` viewTag: 0x${viewTag.toString(16).padStart(2,'0')}`); |
| 191 | + console.log(` stealthAddr: ${stealthAddr}`); |
| 192 | + console.log(` metadata: ${bytesToHex(meta)}`); |
| 193 | + |
| 194 | + return { |
| 195 | + schemeId: 1, |
| 196 | + stealthAddress: stealthAddr, |
| 197 | + caller: pubToAddr(Buffer.from(ephPub)), |
| 198 | + ephemeralPubKey: bytesToHex(ephPub), |
| 199 | + metadata: bytesToHex(meta), |
| 200 | + _yours: true, |
| 201 | + }; |
| 202 | +} |
| 203 | + |
| 204 | +const seeds = [ |
| 205 | + sha256(Buffer.from('eph:0')), |
| 206 | + sha256(Buffer.from('eph:1')), |
| 207 | + sha256(Buffer.from('eph:2')), |
| 208 | + sha256(Buffer.from('eph:3')), |
| 209 | + sha256(Buffer.from('eph:4')), |
| 210 | +]; |
| 211 | + |
| 212 | +const fixtures = seeds.map((s,i) => makeAnnouncement(s, YOURS_INDICES.has(i))); |
| 213 | + |
| 214 | +console.log('\n=== FIXTURE_ANNOUNCEMENTS (paste into scanner.js) ==='); |
| 215 | +console.log(JSON.stringify(fixtures, (k,v) => typeof v==='bigint'?v.toString():v, 2)); |
0 commit comments