// Proves the post-registration sign-in fix: a wallet whose position is NOT yet in the // cached chain index (the state a member is in for the first minute after joining) can // still sign in, via a live contract read of the id from their registration receipt, // and only when the contract agrees that wallet owns that id. import { createRequire } from 'node:module'; import crypto from 'node:crypto'; import fs from 'node:fs'; const ROOT = 'D:/Projects/HighRisk/The RM Circle/promos/'; const require = createRequire(ROOT + 'package.json'); const secp = require(ROOT + 'vendor/secp256k1.js'); const { keccak256 } = require(ROOT + 'vendor/sha3.js'); secp.utils.hmacSha256Sync = (key, ...msgs) => { const h = crypto.createHmac('sha256', Buffer.from(key)); msgs.forEach(m => h.update(Buffer.from(m))); return Uint8Array.from(h.digest()); }; const messages = require(ROOT + 'messages.js'); const DIR = 'D:/tmp/rmc-signin-test'; fs.rmSync(DIR, { recursive: true, force: true }); fs.mkdirSync(DIR, { recursive: true }); // a wallet we control const priv = Buffer.from('59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d', 'hex'); const pub = secp.getPublicKey(priv, false); const ADDR = '0x' + keccak256(Buffer.from(pub.slice(1))).slice(-40); // a chain stub in the exact state that bit #787: the index has NOT seen this member yet let liveReads = 0, seeded = false; const chain = { memberIdByAccount: () => (seeded ? 4242 : null), // cache miss until verifyMember seeds it verifyMember: async (id) => { liveReads++; seeded = true; return id === 4242 ? { registered: true, account: ADDR } : { registered: false }; }, isInTeam: () => true }; messages.init({ dataDir: DIR, chain, isProd: false }); const sign = (msg) => { const m = Buffer.from(msg, 'utf8'); const pre = Buffer.from('\x19Ethereum Signed Message:\n' + m.length, 'utf8'); const digest = Buffer.from(keccak256(Buffer.concat([pre, m])), 'hex'); const [sig, rec] = secp.signSync(digest, priv, { recovered: true, der: false }); return '0x' + Buffer.from(sig).toString('hex') + (27 + rec).toString(16).padStart(2, '0'); }; const ok = [], bad = []; const t = (n, c, extra) => { (c ? ok : bad).push(n + (c || !extra ? '' : ' -> ' + extra)); }; // 1. the old behaviour: no id hint, index cold -> refused let ch = messages.makeChallenge(ADDR); let r = await messages.verifyChallenge(ADDR, sign(ch)); t('cold index with no hint is refused', !!r.error && /No RM Circle position/.test(r.error), JSON.stringify(r)); t('no live read was attempted without a hint', liveReads === 0); // 2. the fix: same cold index, but the receipt id is passed ch = messages.makeChallenge(ADDR); r = await messages.verifyChallenge(ADDR, sign(ch), '4242'); t('cold index WITH the receipt id signs in', !!r.token && r.id === 4242, JSON.stringify(r)); t('it did a live contract read', liveReads === 1); // 3. the hint cannot be abused: a wrong id is refused because the chain disagrees seeded = false; ch = messages.makeChallenge(ADDR); r = await messages.verifyChallenge(ADDR, sign(ch), '9999'); t('a hint for a position this wallet does not own is refused', !!r.error, JSON.stringify(r)); // 4. a forged signature is still refused even with a valid hint seeded = false; ch = messages.makeChallenge(ADDR); const otherPriv = crypto.randomBytes(32); const forge = (msg) => { const m = Buffer.from(msg, 'utf8'); const pre = Buffer.from('\x19Ethereum Signed Message:\n' + m.length, 'utf8'); const digest = Buffer.from(keccak256(Buffer.concat([pre, m])), 'hex'); const [sig, rec] = secp.signSync(digest, otherPriv, { recovered: true, der: false }); return '0x' + Buffer.from(sig).toString('hex') + (27 + rec).toString(16).padStart(2, '0'); }; r = await messages.verifyChallenge(ADDR, forge(ch), '4242'); t('a signature from another wallet is refused', !!r.error && /different account/.test(r.error), JSON.stringify(r)); // 5. once the indexer catches up, no live read is needed seeded = true; liveReads = 0; ch = messages.makeChallenge(ADDR); r = await messages.verifyChallenge(ADDR, sign(ch), '4242'); t('warm index signs in with no live read', !!r.token && liveReads === 0); console.log('PASS ' + ok.length); for (const b of bad) console.log('FAIL ' + b); process.exit(bad.length ? 1 : 0);