From 7314913849013056233f887dc0fca01908391457 Mon Sep 17 00:00:00 2001 From: Anunay Jain Date: Fri, 29 Sep 2023 03:20:44 +0530 Subject: [PATCH] Privacy Preserving Nameswap - DRAFT This test showcases the application of adapter signatures to facilitate privacy-preserving name swaps on handshake. Additionally, this approach can be employed for privacy-preserving cross-chain swaps. --- test/privacy-swap-test.js | 367 ++++++++++++++++++++++++++++++++++++++ test/util/adaptor-sig.js | 269 ++++++++++++++++++++++++++++ 2 files changed, 636 insertions(+) create mode 100644 test/privacy-swap-test.js create mode 100644 test/util/adaptor-sig.js diff --git a/test/privacy-swap-test.js b/test/privacy-swap-test.js new file mode 100644 index 000000000..6e9dca426 --- /dev/null +++ b/test/privacy-swap-test.js @@ -0,0 +1,367 @@ +/* eslint-env mocha */ +/* eslint prefer-arrow-callback: "off" */ + + +'use strict'; + +const assert = require('bsert'); +const Network = require('../lib/protocol/network'); +const FullNode = require('../lib/node/fullnode'); +const MTX = require('../lib/primitives/mtx'); +const Address = require('../lib/primitives/address'); +const Output = require('../lib/primitives/output'); +const {Script, Stack} = require('../lib/script'); +const rules = require('../lib/covenants/rules'); +const {types} = rules; +const {Resource} = require('../lib/dns/resource'); +const {WalletClient} = require('hs-client'); +const Coin = require('../lib/primitives/coin'); +const common = require('../lib/script/common'); +const Opcode = require('../lib/script/opcode.js'); + +const secp256k1 = require('bcrypto/lib/js/secp256k1'); + +const adaptor = require('./util/adaptor-sig'); + +const network = Network.get('regtest'); + +const ports = { + p2p: 14331, + node: 14332, + wallet: 14333 +}; + +const node = new FullNode({ + memory: true, + network: 'regtest', + plugins: [require('../lib/wallet/plugin')], + env: { + 'HSD_WALLET_HTTP_PORT': ports.wallet.toString() + } +}); + +const wclient = new WalletClient({ + port: ports.wallet +}); + +const {wdb} = node.require('walletdb'); + +let alice, bob, aliceReceive, bobReceive; + +const name = rules.grindName(5, 1, network); +const nameHash = rules.hashName(name); +const price = 5 * 1e6; // 5 HNS + +// glob is used to store communication data between Bob and Alex +let glob; + +async function mineBlocks(n, addr) { + addr = addr ? addr : new Address().toString('regtest'); + for (let i = 0; i < n; i++) { + const block = await node.miner.mineBlock(null, addr); + await node.chain.add(block); + } +} + +function createMultisig(key1, key2){ + return new Script([ + Opcode.fromInt(2), + Opcode.fromPush(key1), + Opcode.fromPush(key2), + Opcode.fromInt(2), + Opcode.fromSymbol('checkmultisig'), + ]); +} + +function createAnyoneCanFinalizeMultisig(key1, key2){ + return new Script([ + Opcode.fromSymbol('type'), + Opcode.fromInt(rules.types.TRANSFER), + Opcode.fromSymbol('equal'), + + Opcode.fromSymbol('if'), + Opcode.fromInt(2), + Opcode.fromPush(key1), + Opcode.fromPush(key2), + Opcode.fromInt(2), + Opcode.fromSymbol('checkmultisig'), + + Opcode.fromSymbol('else'), + + Opcode.fromSymbol('type'), + Opcode.fromInt(rules.types.FINALIZE), + Opcode.fromSymbol('equal'), + Opcode.fromSymbol('endif'), + ]); +} + +describe('Privacy preserving name swap', function() { + before(async () => { + await node.open(); + await wclient.open(); + + alice = await wdb.create(); + bob = await wdb.create(); + + aliceReceive = await alice.receiveAddress(); // Will recieve funds at this address + bobReceive = await bob.receiveAddress(); // Will recieve name at this address + }); + + after(async () => { + await wclient.close(); + await node.close(); + }); + + it('should fund both wallets', async () => { + await mineBlocks(2, aliceReceive); + await mineBlocks(2, bobReceive); + + // Wallet rescan is an effective way to ensure that + // wallet and chain are synced before proceeding. + await wdb.rescan(0); + + const aliceBal = await alice.getBalance(); + const bobBal = await bob.getBalance(); + assert(aliceBal.confirmed === 2000 * 2 * 1e6); + assert(bobBal.confirmed === 2000 * 2 * 1e6); + }); + + it('should win name with Alice\'s wallet', async () => { + await alice.sendOpen(name, false); + await mineBlocks(network.names.treeInterval + 1); + + await alice.sendBid(name, 100000, 200000); + await mineBlocks(network.names.biddingPeriod); + + await alice.sendReveal(name); + await mineBlocks(network.names.revealPeriod + 1); + + const ns = await alice.getNameStateByName(name); + assert(ns); + const owner = ns.owner; + const coin = await alice.getCoin(owner.hash, owner.index); + assert(coin); + const json = ns.getJSON(node.chain.height, node.network); + assert(json.state === 'CLOSED'); + }); + + it('should REGISTER', async () => { + const resource = Resource.fromJSON({ + records: [{type: 'TXT', txt: ['Contact Alice to buy this name!']}] + }); + await alice.sendUpdate(name, resource); + await mineBlocks(network.names.treeInterval); + }); + + it('should generate multisig contracts', async () => { + // We make two multisig, one will hold the funds, the other will hold the name + // These will probably be deterministic? + const aliceKeys = [secp256k1.privateKeyGenerate(), secp256k1.privateKeyGenerate()]; + const bobKeys = [secp256k1.privateKeyGenerate(), secp256k1.privateKeyGenerate()]; + + // Alice and Bob exchange pubkeys + const alicePubKeys = aliceKeys.map(k => secp256k1.publicKeyCreate(k)); + const bobPubKeys = bobKeys.map(k => secp256k1.publicKeyCreate(k)); + + const nameMultisig = createAnyoneCanFinalizeMultisig(alicePubKeys[0], bobPubKeys[0]); + const fundMultisig = createMultisig(alicePubKeys[1], bobPubKeys[1]); + + glob = { + aliceKeys, + bobKeys, + alicePubKeys, + bobPubKeys, + nameMultisig, + fundMultisig, + } + + }); + + // Before transferring the name, both parties will create a refund presign in case + // the other party leaves, the refund presign will have a future locktime + it('should create presigns for a refund transaction', async () => { + // TODO + }) + + it('should TRANSFER/FINALIZE to name address', async () => { + const heightBeforeTransfer = node.chain.height; + const address = Address.fromScript(glob.nameMultisig); + await alice.sendTransfer(name, address); + await mineBlocks(network.names.transferLockup); + + let ns = await node.getNameStatus(nameHash); + assert.strictEqual(ns.transfer, heightBeforeTransfer + 1); + + await alice.sendFinalize(name); + await mineBlocks(1); + + ns = await node.getNameStatus(nameHash); + + const {hash, index} = ns.owner; + const coin = await node.getCoin(hash, index); + assert.deepStrictEqual(coin.address, address); + }); + + it('should fund the fund address - bob', async () => { + const address = Address.fromScript(glob.fundMultisig); + const out = new Output({ + address: address, + value: price + }); + glob.fundingTX = await bob.send({ + outputs: [out] + }); + await mineBlocks(1); + }); + + it('should verify both addresses are correctly funded', async () => { + // TODO + }) + + it('should create presigns', async () => { + const mtx = new MTX(); + const output = new Output(); + output.address = aliceReceive; + output.value = price + mtx.addOutput(output); + mtx.addCoin(Coin.fromTX(glob.fundingTX, 0, -1)); + + glob.fundTX = mtx; + }) + + it('should verify fundTX', async () => { + // TODO + }) + + it('should create TRANSFER presigns', async () => { + const ns = await node.getNameStatus(nameHash); + const mtx = new MTX(); + + const {hash, index} = ns.owner; + const nameCoin = await node.getCoin(hash, index); + mtx.addCoin(nameCoin); + + const nameAddress = Address.fromScript(glob.nameMultisig); + const output = new Output(); + output.address = nameAddress; + output.value = ns.value; + + const address = bobReceive; + output.covenant.type = types.TRANSFER; + output.covenant.pushHash(nameHash); + output.covenant.pushU32(ns.height); + output.covenant.pushU8(address.version); + output.covenant.push(address.hash); + mtx.outputs.push(output); + glob.transferTX = mtx; + }); + + + it('should verify TRANSFER/FINALIZE presigns', async () => { + // TODO + }); + + + it('should sign TRANSFER presign - Alice', async () => { + const { + ALL, + ANYONECANPAY + } = common.hashType; + + const ns = await node.getNameStatus(nameHash); + + const sighashName = glob.transferTX.signatureHash(0, glob.nameMultisig, ns.value, ALL | ANYONECANPAY); + const [t, T] = adaptor.generateTweakPoint(); + const sige = adaptor.signTweaked(sighashName, glob.aliceKeys[0], T); + glob.t = t; + glob.T = T + glob.sige_transfer = sige; + // pass [T, sige] to Bob + }); + + it('should verify encrypted signature - Bob', async () => { + const { + ALL, + ANYONECANPAY + } = common.hashType; + const ns = await node.getNameStatus(nameHash); + + const [P, Q, se, proof] = glob.sige_transfer; + const sighashName = glob.transferTX.signatureHash(0, glob.nameMultisig, ns.value, ALL | ANYONECANPAY); + assert(adaptor.verifyTweakedSignature(sighashName, P, Q, se, proof, glob.T, glob.alicePubKeys[0])); + }); + + it('should sign funding transaction - bob', async () => { + const { + ALL, + ANYONECANPAY + } = common.hashType; + + const sighashFund = glob.fundTX.signatureHash(0, glob.fundMultisig, price, ALL | ANYONECANPAY); + const sige = adaptor.signTweaked(sighashFund, glob.bobKeys[1], glob.T); + glob.sige_fund = sige; + // pass this to Alice + }) + + it('should verify encrypted signature - Alice', async () => { + const { + ALL, + ANYONECANPAY + } = common.hashType; + + const [P, Q, se, proof] = glob.sige_fund; + const sighashFund = glob.fundTX.signatureHash(0, glob.fundMultisig, price, ALL | ANYONECANPAY); + assert(adaptor.verifyTweakedSignature(sighashFund, P, Q, se, proof, glob.T, glob.bobPubKeys[1])); + }); + + it('should reveal signature fundTX - Alice', async () => { + const { + ALL, + ANYONECANPAY + } = common.hashType; + const ns = await node.getNameStatus(nameHash); + + const aliceSig = glob.fundTX.signature(0, glob.fundMultisig, price, glob.aliceKeys[1], ALL); + + const [P, Q, se, proof] = glob.sige_fund; + const bobSig = adaptor.untweakCompact(Q, se, glob.t, ALL | ANYONECANPAY); + + const witness = new Stack(); + witness.pushInt(0); + witness.push(aliceSig); + witness.push(bobSig); + witness.push(glob.fundMultisig.encode()); + glob.fundTX.inputs[0].witness.fromStack(witness); + + assert(glob.fundTX.verify()); + }); + + it('should recover t from fundTX signature - Alice', async () => { + const { + ALL, + ANYONECANPAY + } = common.hashType; + + const ns = await node.getNameStatus(nameHash); + const bobSig = glob.transferTX.signature(0, glob.nameMultisig, ns.value, glob.bobKeys[0], ALL); + + const untweaked_sig = glob.fundTX.inputs[0].witness.toArray()[2]; + const [r, s] = secp256k1._decodeCompact(untweaked_sig.slice(0, -1)) + const [P, Q, se, proof] = glob.sige_transfer; + const [t, ] = adaptor.extractTweakPoint(s, glob.sige_fund[2]) + + const aliceSig = adaptor.untweakCompact(Q, se, t, ALL | ANYONECANPAY); + + const witness = new Stack(); + witness.pushInt(0); + witness.push(aliceSig); + witness.push(bobSig); + witness.push(glob.nameMultisig.encode()); + glob.transferTX.inputs[0].witness.fromStack(witness); + + assert(glob.transferTX.verify()); + }); + +}); + + diff --git a/test/util/adaptor-sig.js b/test/util/adaptor-sig.js new file mode 100644 index 000000000..de61208fd --- /dev/null +++ b/test/util/adaptor-sig.js @@ -0,0 +1,269 @@ +const secp256k1 = require('bcrypto/lib/js/secp256k1'); +const rng = require('crypto'); +const HmacDRBG = require('bcrypto/lib/js/hmac-drbg'); +const blake2b = require('bcrypto/lib/blake2b'); +const bio = require('bufio'); + + +/* +* This code uses unaudited experimental cryptography, using this code will likely +* end up with you writing crpyto libraries at Oracle. +* References: +* [ADAPTOR-SIG] https://github.com/LLFourn/one-time-VES/blob/master/main.pdf +* [BIP-SCHNORR] https://github.com/sipa/bips/blob/d194620/bip-schnorr.mediawiki +* [BCRYPTO-ECDSA] https://github.com/bcoin-org/bcrypto/blob/master/lib/js/ecdsa.js +*/ +class Adaptor { + + generateTweakPoint(){ + // This is effectively a private and public key pair + const G = secp256k1.curve.g; + const t = secp256k1.curve.randomScalar(rng); + const T = G.mulBlind(t); + return [t, T]; + } + + signTweaked(msg, key, T) { + const { + n + } = secp256k1.curve; + const G = secp256k1.curve.g; + const a = secp256k1.curve.decodeScalar(key); + + if (a.isZero() || a.cmp(n) >= 0) + throw new Error('Invalid private key.'); + + const m = secp256k1._reduce(msg); + const nonce = secp256k1.curve.encodeScalar(m); + const drbg = new HmacDRBG(secp256k1.hash, key, nonce); + + for (;;) { + const bytes = drbg.generate(secp256k1.curve.scalarSize); + const k = secp256k1._truncate(bytes); + + if (k.isZero() || k.cmp(n) >= 0) + continue; + + // P = kG + // Q = ktG = tP + + // note that k must remain secret + const P = G.mulBlind(k) + const Q = T.mulBlind(k); + + if (Q.isInfinity()) + continue; + + const q = Q.getX().mod(n); + + if (q.isZero()) + continue; + + // The code would usually look like this: + + // const k_inverse = k.fermat(n); + // const s_numerator = q.mul(a).add(m).mod(n); + // const s_tweaked = s_numerator.mul(k_inverse).mod(n); + + // but to protect from side channel attacks, + // we'll use a random integer to mess with timings + + const b = secp256k1.curve.randomScalar(rng); + const kb_inverse = k.mul(b).fermat(n); + const bm = m.mul(b).mod(n); + const ba = a.mul(b).mod(n); + const s_numerator = q.mul(ba).add(bm).mod(n); + const s_tweaked = s_numerator.mul(kb_inverse).mod(n); + + const proof = this.generateDLEQ(P, Q, T, k); + return [P, Q, s_tweaked, proof]; + } + } + + generateDLEQ(P, Q, T, k){ + const { n } = secp256k1.curve; + const G = secp256k1.curve.g; + + const p = P.getX().mod(n); + const q = Q.getX().mod(n); + + for(;;){ + // DLEQ proof + // It is important r2 stays secret, can probably be deteminstic, + // don't wanna mess with determinstic stuff rn though + // Will need extra protections against side channel attacks if deteministic + // it is also important r2 is not reused since a implementaion like this + // will leak k (which will leak to private key leakage) + + const r2 = secp256k1.curve.randomScalar(rng); + const G_r2 = G.mulBlind(r2); + const T_r2 = T.mulBlind(r2); + + if (G_r2.isInfinity() || T_r2.isInfinity()) + continue; + + const dp = G_r2.getX(); + const dq = T_r2.getX(); + + if (dp.isZero() || dq.isZero()) + continue; + + const arr = [p.toBuffer(), q.toBuffer(), dp.toBuffer(), dq.toBuffer()]; + const hash = blake2b.digest(Buffer.concat(arr)); + const e = secp256k1._truncate(hash).mod(n); + // random oracle + if (e.isZero() || e.cmp(n) >= 0) + continue; + + const ke = k.mul(e).mod(n); + const pi = r2.add(ke).mod(n); + + // if r2 is leaked, pi can be used to calculate k + // as k = (pi - r2)/e + // which would leak the private key + + return { + dp, + dq, + pi, + } + } + } + + verifyDLEQ(P, Q, T, proof){ + const {p, n} = secp256k1.curve; + const G = secp256k1.curve.g; + + const {dp, dq, pi} = proof; + // DLEQ proofs are basically schnorr signatures + // BIP schnorr + if(dp.isZero() || dp.cmp(p) >= 0) + return false; + + if(dq.isZero() || dq.cmp(p) >= 0) + return false; + + const P_point = P.getX().mod(n); + const Q_point = Q.getX().mod(n); + // Oracle + // This can probably be imporved but works well enough for a PoC + const arr = [P_point.toBuffer(), Q_point.toBuffer(), dp.toBuffer(), dq.toBuffer()]; + const hash = blake2b.digest(Buffer.concat(arr)); + const e = secp256k1._truncate(hash); + + // e really shouldn't be zero but just in case + if (e.isZero() || e.cmp(n) >= 0) + return false; + + const e_negative = e.neg().mod(n); + // P = kG + // Q = ktG = kT + // pi = r2 + ke + // r2*G = pi*G - e*P + // r2*T = pi*T - e*Q + // Since e depends on r2G and r2T + // one cannot just select a e and pass the corresponding + // points as proof. + + const G_r2 = G.mulAdd(pi, P, e_negative); + const T_r2 = T.mulAdd(pi, Q, e_negative); + + if(G_r2.isInfinity() || T_r2.isInfinity()) + return false; + + return G_r2.eqR(dp) && T_r2.eqR(dq); + } + + verifyTweakedSignature(msg, P, Q, se, proof, T, pubKey){ + const {n} = secp256k1.curve; + const G = secp256k1.curve.g; + const m = secp256k1._reduce(msg); + const A = secp256k1.curve.decodePoint(pubKey); + + const p = P.getX().mod(n); + const q = Q.getX().mod(n); + + if (p.isZero() || p.cmp(n) >= 0) + return false; + if (q.isZero() || q.cmp(n) >= 0) + return false; + if (se.isZero() || se.cmp(n) >= 0) + return false; + + if(!this.verifyDLEQ(P, Q, T, proof)) + return false; + + const si = se.invert(n); + + const u1 = m.mul(si).mod(n); + const u2 = q.mul(si).mod(n); + + // Shamir's trick + const R = G.mulAdd(u1, A, u2); + return R.eqR(p); + } + + untweakSignature(Q, se, t){ + const { n , nh } = secp256k1.curve; + const t_inverse = t.fermat(n); + // s = se / t + const s = se.mul(t_inverse).mod(n); + const r = Q.getX().mod(n); + + // BIP 66, return LOW_S + if (s.cmp(nh) > 0) { + s.ineg().imod(n); + } + + return [r, s]; + } + + extractTweakPoint(s, se){ + // s = se / t + const { n } = secp256k1.curve; + const s_inverse = s.invert(n); + const t = se.mul(s_inverse).mod(n); + const T = secp256k1.curve.g.mul(t); + return [t, T]; + } + + // Helper + + untweakCompact(Q, se, t, type){ + const [r, s] = this.untweakSignature(Q, se, t); + const sig = secp256k1._encodeCompact(r, s); + const bw = bio.write(65); + bw.writeBytes(sig); + bw.writeU8(type); + return bw.render(); + } +} + + +module.exports = new Adaptor(); + +// Example usage + + +// const adaptor = new Adaptor(); + +// const msg = Buffer.from("deadbeef", 'hex') +// const key = secp256k1.privateKeyGenerate(); +// const pubKey = secp256k1.publicKeyCreate(key); + +// // You can literally use a pub/private key for this +// [t, T] = adaptor.generateTweakPoint(); + +// [P, Q, se, proof] = adaptor.signTweaked(msg, key, T); + +// console.log(adaptor.verifyTweakedSignature(msg, P, Q, se, proof, T, pubKey)); + +// const [r, s] = adaptor.untweakSignature(Q, se, t); + +// [t_extracted, T_extracted] = adaptor.extractTweakPoint(s, se); + +// // t_extracted may not be equal to orignal t (it might be equal to -t mod n), +// // but it doesn't matter for untweaking (but it matters for obtaining the orignal point T) + + +// console.log(secp256k1._verify(msg, r, s, pubKey));