Skip to content

Commit

Permalink
Merge PR #767 from 'rithvikvibhu/multisig-generatenonce'
Browse files Browse the repository at this point in the history
  • Loading branch information
pinheadmz committed Dec 21, 2022
2 parents 234a597 + 5f48556 commit b456147
Show file tree
Hide file tree
Showing 8 changed files with 339 additions and 26 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ to these calls.
- `createbatch` and `sendbatch` create batch transactions with any number
of outputs with any combination of covenants.

- Updates related to nonces and blinds
- Multisig wallets will compute nonces based on the LOWEST public key in the group.
This makes multiparty bidding and revealing more deteministic. Older versions would
always use the wallet's OWN public key. To preserve compatability with older software:
- RPC method `importnonce` now returns an array of blinds instead of a single blind.
- HTTP endpoint `/wallet/:id/nonce/:name`'s response replaces 2 string fields (`nonce`, `blind`) with arrays of the same type (`nonces`, `blinds`)

## v4.0.0

**When upgrading to this version of hsd you must pass
Expand Down
8 changes: 4 additions & 4 deletions lib/wallet/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -1029,13 +1029,13 @@ class HTTP extends Server {
}

const nameHash = rules.hashName(name);
const nonce = await req.wallet.generateNonce(nameHash, address, bid);
const blind = rules.blind(bid, nonce);
const nonces = await req.wallet.generateNonces(nameHash, address, bid);
const blinds = nonces.map(nonce => rules.blind(bid, nonce));

return res.json(200, {
address: address.toString(this.network),
blind: blind.toString('hex'),
nonce: nonce.toString('hex'),
blinds: blinds.map(blind => blind.toString('hex')),
nonces: nonces.map(nonce => nonce.toString('hex')),
bid: bid,
name: name,
nameHash: nameHash.toString('hex')
Expand Down
4 changes: 2 additions & 2 deletions lib/wallet/rpc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2666,9 +2666,9 @@ class RPC extends RPCBase {
const nameHash = rules.hashName(name);
const address = parseAddress(addr, this.network);

const blind = await wallet.generateBlind(nameHash, address, value);
const blinds = await wallet.generateBlinds(nameHash, address, value);

return blind.toString('hex');
return blinds.map(blind => blind.toString('hex'));
}
}

Expand Down
78 changes: 68 additions & 10 deletions lib/wallet/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -1195,16 +1195,14 @@ class Wallet extends EventEmitter {
}

/**
* Generate nonce deterministically
* based on address, name hash, and
* bid value.
* @param {Buffer} nameHash
* Get public keys at index based on
* address and value for nonce generation
* @param {Address} address
* @param {Amount} value
* @returns {Buffer}
* @returns {Promise<Buffer[]>} public keys
*/

async generateNonce(nameHash, address, value) {
async _getNoncePublicKeys(address, value) {
const path = await this.getPath(address.hash);

if (!path)
Expand All @@ -1219,28 +1217,88 @@ class Wallet extends EventEmitter {
const lo = value >>> 0;
const index = (hi ^ lo) & 0x7fffffff;

const {publicKey} = account.accountKey.derive(index);
const publicKeys = [];
for (const accountKey of [account.accountKey, ...account.keys])
publicKeys.push(accountKey.derive(index).publicKey);

// Use smallest public key
publicKeys.sort(Buffer.compare);

return publicKeys;
}

/**
* Generate nonce deterministically
* based on address (smallest pubkey),
* name hash, and bid value.
* @param {Buffer} nameHash
* @param {Address} address
* @param {Amount} value
* @returns {Promise<Buffer>}
*/

async generateNonce(nameHash, address, value) {
const publicKeys = await this._getNoncePublicKeys(address, value);
return blake2b.multi(address.hash, publicKeys[0], nameHash);
}

/**
* Generate nonces deterministically
* for all keys (in multisig).
* @param {Buffer} nameHash
* @param {Address} address
* @param {Amount} value
* @returns {Promise<Buffer[]>}
*/

async generateNonces(nameHash, address, value) {
const publicKeys = await this._getNoncePublicKeys(address, value);

return blake2b.multi(address.hash, publicKey, nameHash);
// Generate nonces for all public keys
const nonces = [];
for (const publicKey of publicKeys)
nonces.push(blake2b.multi(address.hash, publicKey, nameHash));

return nonces;
}

/**
* Generate nonce & blind, save nonce.
* @param {Buffer} nameHash
* @param {Address} address
* @param {Amount} value
* @returns {Buffer}
* @returns {Promise<Buffer>}
*/

async generateBlind(nameHash, address, value) {
const nonce = await this.generateNonce(nameHash, address, value);
const blind = rules.blind(value, nonce);

await this.txdb.saveBlind(blind, {value, nonce});

return blind;
}

/**
* Generate all nonces & blinds, save nonces.
* @param {Buffer} nameHash
* @param {Address} address
* @param {Amount} value
* @returns {Promise<Buffer[]>}
*/

async generateBlinds(nameHash, address, value) {
const nonces = await this.generateNonces(nameHash, address, value);

const blinds = [];
for (const nonce of nonces) {
const blind = rules.blind(value, nonce);
await this.txdb.saveBlind(blind, {value, nonce});
blinds.push(blind);
}

return blinds;
}

/**
* Make a claim MTX.
* @param {String} name
Expand Down
4 changes: 2 additions & 2 deletions test/auction-rpc-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,11 +336,11 @@ describe('Auction RPCs', function() {
// that same value. Note that "loser" MUST remember their original
// bid value. If this were a wallet recovery scenario, that value
// would have to be entered by the user without data from the blockchain.
const importedBlind = await util.wrpc(
const importedBlinds = await util.wrpc(
'importnonce',
[bidName, bidAddress, loserBid.bid]
);
assert.strictEqual(importedBlind, bidBlind);
assert.strictEqual(importedBlinds[0], bidBlind);
});

it('should create REVEAL with signing paths', async () => {
Expand Down
16 changes: 8 additions & 8 deletions test/wallet-http-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -463,13 +463,13 @@ describe('Wallet HTTP', function() {
const nameHash = rules.hashName(name);

const primary = node.plugins.walletdb.wdb.primary;
const nonce = await primary.generateNonce(nameHash, address, bid);
const blind = rules.blind(bid, nonce);
const nonces = await primary.generateNonces(nameHash, address, bid);
const blinds = nonces.map(nonce => rules.blind(bid, nonce));

assert.deepStrictEqual(response, {
address: address.toString(network.type),
blind: blind.toString('hex'),
nonce: nonce.toString('hex'),
blinds: blinds.map(blind => blind.toString('hex')),
nonces: nonces.map(nonce => nonce.toString('hex')),
bid: bid,
name: name,
nameHash: nameHash.toString('hex')
Expand All @@ -488,13 +488,13 @@ describe('Wallet HTTP', function() {
const nameHash = rules.hashName(name);

const primary = node.plugins.walletdb.wdb.primary;
const nonce = await primary.generateNonce(nameHash, address, bid);
const blind = rules.blind(bid, nonce);
const nonces = await primary.generateNonces(nameHash, address, bid);
const blinds = nonces.map(nonce => rules.blind(bid, nonce));

assert.deepStrictEqual(response, {
address: address.toString(network.type),
blind: blind.toString('hex'),
nonce: nonce.toString('hex'),
blinds: blinds.map(blind => blind.toString('hex')),
nonces: nonces.map(nonce => nonce.toString('hex')),
bid: bid,
name: name,
nameHash: nameHash.toString('hex')
Expand Down
116 changes: 116 additions & 0 deletions test/wallet-rpc-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1024,4 +1024,120 @@ describe('Wallet RPC Methods', function() {
assert.strictEqual(json.details[0].fee, amount * -1); // fees are negative
});
});

describe('Multisig Auction RPC', function() {
// wallet clients
let alice, bob;

// auction
let name;
const bidValue = 5, blindValue = 5;

async function signMultisigTx(tx, walletClients) {
assert(tx.hex, 'tx must be a json object with `hex`');
assert(walletClients.length);

for (const wclient of walletClients)
tx = await wclient.sign({tx: tx.hex});

return tx;
}

before(async () => {
await wclient.createWallet('msAlice', {
type: 'multisig',
m: 2,
n: 2
});
await wclient.createWallet('msBob', {
type: 'multisig',
m: 2,
n: 2
});

alice = wclient.wallet('msAlice');
bob = wclient.wallet('msBob');

// Initialize both multisig wallets
const accountKeys = {
alice: (await alice.getAccount('default')).accountKey,
bob: (await bob.getAccount('default')).accountKey
};
await alice.addSharedKey('default', accountKeys.bob);
await bob.addSharedKey('default', accountKeys.alice);

// Fund wallet
await wclient.execute('selectwallet', ['msAlice']);
const addr = await wclient.execute('getnewaddress', []);
await nclient.execute('generatetoaddress', [100, addr]);
});

it('(alice) should open name for auction', async () => {
await wclient.execute('selectwallet', ['msAlice']);

// Create, sign, send OPEN
name = await nclient.execute('grindname', [5]);
const tx = await wclient.execute('createopen', [name]);
const txSigned = await signMultisigTx(tx, [alice, bob]);
await nclient.execute('sendrawtransaction', [txSigned.hex]);

// confirm and advance to bidding phase
const addrAlice = await wclient.execute('getnewaddress', []);
await nclient.execute('generatetoaddress', [treeInterval + 1, addrAlice]);
});

it('(alice) should bid on name with blind', async () => {
await wclient.execute('selectwallet', ['msAlice']);

// Create, sign, send BID
const tx = await wclient.execute(
'createbid',
[name, bidValue, bidValue + blindValue]
);
const txSigned = await signMultisigTx(tx, [alice, bob]);
await nclient.execute('sendrawtransaction', [txSigned.hex]);

// confirm and advance to reveal phase
const addrAlice = await wclient.execute('getnewaddress', []);
await nclient.execute('generatetoaddress', [biddingPeriod + 1, addrAlice]);
});

it('(bob) should not be able to reveal bid', async () => {
// Alice can create reveal
await wclient.execute('selectwallet', ['msAlice']);
assert.doesNotReject(wclient.execute('createreveal', [name]));

// Bob cannot.
await wclient.execute('selectwallet', ['msBob']);
assert.rejects(
wclient.execute('createreveal', [name]),
{message: `No bids to reveal for name: ${name}.`}
);
});

it('(bob) should import nonce', async () => {
await wclient.execute('selectwallet', ['msBob']);
const bidsBob = await wclient.execute('getbids', [name, true, true]);
const address = bidsBob[0].address;
const blinds = await wclient.execute('importnonce', [name, address, 5]);
assert.strictEqual(blinds[0], bidsBob[0].blind);
});

it('(bob) should reveal bid', async () => {
await wclient.execute('selectwallet', ['msBob']);

// Create, sign, send REVEAL
const tx = await wclient.execute('createreveal', [name]);
const txSigned = await signMultisigTx(tx, [alice, bob]);
await nclient.execute('sendrawtransaction', [txSigned.hex]);

// confirm and advance to close auction
const addrAlice = await wclient.execute('getnewaddress', []);
await nclient.execute('generatetoaddress', [revealPeriod + 1, addrAlice]);

// Ensure name is owned
const ownedNames = await wclient.execute('getnames', [true]);
assert.strictEqual(ownedNames.length, 1);
});
});
});
Loading

0 comments on commit b456147

Please sign in to comment.