From 013708d0ac45625d0324c507f0519f6db6b4197d Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Fri, 15 Nov 2019 20:08:49 -0800 Subject: [PATCH] Fix support for validator key rotation, add end-to-end test (#1643) --- .circleci/config.yml | 10 +- .../src/e2e-tests/governance_tests.ts | 284 +++++++++--- packages/celotool/src/e2e-tests/utils.ts | 2 +- packages/cli/package.json | 1 + .../src/commands/account/authorize.test.ts | 24 +- .../cli/src/commands/account/authorize.ts | 33 +- .../cli/src/commands/account/claims.test.ts | 2 +- .../commands/account/proof-of-possession.ts | 28 ++ packages/cli/src/commands/account/register.ts | 11 +- .../cli/src/commands/validator/publicKey.ts | 43 -- .../cli/src/commands/validator/register.ts | 17 +- .../validator/update-bls-public-key.ts | 34 ++ packages/cli/src/utils/checks.ts | 2 +- packages/cli/src/utils/command.ts | 35 +- packages/cli/src/utils/helpers.ts | 22 - .../contractkit/src/wrappers/Accounts.test.ts | 74 ++++ packages/contractkit/src/wrappers/Accounts.ts | 130 ++++-- .../src/wrappers/Validators.test.ts | 17 +- .../contractkit/src/wrappers/Validators.ts | 51 ++- .../docs/command-line-interface/account.md | 41 +- .../docs/command-line-interface/validator.md | 57 +-- packages/mobile/src/identity/commentKey.ts | 2 +- packages/mobile/src/identity/verification.ts | 7 +- packages/mobile/src/import/saga.ts | 2 +- packages/mobile/src/invite/saga.ts | 2 +- packages/mobile/src/qrcode/utils.ts | 2 +- .../mobile/src/recipients/RecipientPicker.tsx | 2 +- .../mobile/src/verify/VerificationCodeRow.tsx | 2 +- .../protocol/contracts/common/Accounts.sol | 419 +++++++++--------- .../contracts/common/UsingPrecompiles.sol | 10 +- .../contracts/common/interfaces/IAccounts.sol | 8 +- .../contracts/governance/Election.sol | 10 +- .../contracts/governance/Governance.sol | 6 +- .../contracts/governance/Validators.sol | 248 ++++++++--- .../governance/interfaces/IElection.sol | 2 +- .../governance/interfaces/IValidators.sol | 2 + .../governance/test/MockElection.sol | 2 +- .../governance/test/MockValidators.sol | 27 +- .../governance/test/ValidatorsTest.sol | 8 +- .../contracts/identity/Attestations.sol | 4 +- packages/protocol/lib/test-utils.ts | 25 -- packages/protocol/lib/web3-utils.ts | 25 +- packages/protocol/migrations/10_accounts.ts | 18 +- .../migrations/20_elect_validators.ts | 34 +- packages/protocol/package.json | 1 - packages/protocol/test/common/accounts.ts | 63 +-- packages/protocol/test/governance/election.ts | 16 +- .../protocol/test/governance/validators.ts | 330 +++++++++----- .../protocol/test/identity/attestations.ts | 19 +- packages/utils/src/address.ts | 36 +- packages/utils/src/bls.ts | 25 ++ packages/utils/src/io.ts | 2 +- packages/utils/src/signatureUtils.ts | 78 ++-- 53 files changed, 1455 insertions(+), 900 deletions(-) create mode 100644 packages/cli/src/commands/account/proof-of-possession.ts delete mode 100644 packages/cli/src/commands/validator/publicKey.ts create mode 100644 packages/cli/src/commands/validator/update-bls-public-key.ts create mode 100644 packages/contractkit/src/wrappers/Accounts.test.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index d0170e30e55..1a53a694298 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -525,7 +525,7 @@ jobs: command: | set -e cd packages/celotool - ./ci_test_transfers.sh checkout asaj/bls-sign-round + ./ci_test_transfers.sh checkout asaj/key-rotation-plus-enode end-to-end-geth-blockchain-parameters-test: <<: *e2e-defaults @@ -543,7 +543,7 @@ jobs: command: | set -e cd packages/celotool - ./ci_test_blockchain_parameters.sh checkout asaj/bls-sign-round + ./ci_test_blockchain_parameters.sh checkout asaj/key-rotation-plus-enode end-to-end-geth-governance-test: <<: *e2e-defaults @@ -563,7 +563,7 @@ jobs: command: | set -e cd packages/celotool - ./ci_test_governance.sh checkout asaj/bls-sign-round + ./ci_test_governance.sh checkout asaj/key-rotation-plus-enode end-to-end-geth-sync-test: <<: *e2e-defaults @@ -582,7 +582,7 @@ jobs: command: | set -e cd packages/celotool - ./ci_test_sync.sh checkout asaj/bls-sign-round + ./ci_test_sync.sh checkout asaj/key-rotation-plus-enode end-to-end-geth-validator-order-test: <<: *e2e-defaults @@ -600,7 +600,7 @@ jobs: command: | set -e cd packages/celotool - ./ci_test_validator_order.sh checkout asaj/bls-sign-round + ./ci_test_validator_order.sh checkout asaj/key-rotation-plus-enode web: working_directory: ~/app diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index 7f66cc0ee0e..87af3bce44c 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -1,5 +1,7 @@ +// tslint:disable-next-line: no-reference (Required to make this work w/ ts-node) +/// import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' -import { AccountsWrapper } from '@celo/contractkit/lib/wrappers/Accounts' +import { getBlsPoP, getBlsPublicKey } from '@celo/utils/lib/bls' import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import { assert } from 'chai' @@ -8,17 +10,21 @@ import { assertAlmostEqual, getContext, getEnode, + GethInstanceConfig, importGenesis, initAndStartGeth, sleep, } from './utils' +// TODO(asa): Test independent rotation of ecdsa, bls keys. describe('governance tests', () => { const gethConfig = { migrate: true, instances: [ + // Validators 0 and 1 are swapped in and out of the group. { name: 'validator0', validating: true, syncmode: 'full', port: 30303, rpcport: 8545 }, { name: 'validator1', validating: true, syncmode: 'full', port: 30305, rpcport: 8547 }, + // Validator 2 will authorize a validating key every other epoch. { name: 'validator2', validating: true, syncmode: 'full', port: 30307, rpcport: 8549 }, { name: 'validator3', validating: true, syncmode: 'full', port: 30309, rpcport: 8551 }, { name: 'validator4', validating: true, syncmode: 'full', port: 30311, rpcport: 8553 }, @@ -34,7 +40,7 @@ describe('governance tests', () => { let goldToken: any let registry: any let validators: any - let accounts: AccountsWrapper + let accounts: any let kit: ContractKit before(async function(this: any) { @@ -55,7 +61,7 @@ describe('governance tests', () => { registry = await kit._web3Contracts.getRegistry() election = await kit._web3Contracts.getElection() epochRewards = await kit._web3Contracts.getEpochRewards() - accounts = await kit.contracts.getAccounts() + accounts = await kit._web3Contracts.getAccounts() } const unlockAccount = async (address: string, theWeb3: any) => { @@ -79,9 +85,17 @@ describe('governance tests', () => { } } - const getValidatorGroupKeys = async () => { + const getValidatorSigner = (address: string, blockNumber?: number) => { + if (blockNumber) { + return accounts.methods.getValidatorSigner(address).call({}, blockNumber) + } else { + return accounts.methods.getValidatorSigner(address).call() + } + } + + const getValidatorGroupPrivateKey = async () => { const [groupAddress] = await validators.methods.getRegisteredValidatorGroups().call() - const name = await accounts.getName(groupAddress) + const name = await accounts.methods.getName(groupAddress).call() const encryptedKeystore64 = name.split(' ')[1] const encryptedKeystore = JSON.parse(Buffer.from(encryptedKeystore64, 'base64').toString()) // The validator group ID is the validator group keystore encrypted with validator 0's @@ -89,7 +103,7 @@ describe('governance tests', () => { // @ts-ignore const encryptionKey = `0x${gethConfig.instances[0].privateKey}` const decryptedKeystore = web3.eth.accounts.decrypt(encryptedKeystore, encryptionKey) - return [groupAddress, decryptedKeystore.privateKey] + return decryptedKeystore.privateKey } const activate = async (account: string, txOptions: any = {}) => { @@ -103,12 +117,8 @@ describe('governance tests', () => { return tx.send({ from: account, ...txOptions, gas }) } - const removeMember = async ( - groupWeb3: any, - group: string, - member: string, - txOptions: any = {} - ) => { + const removeMember = async (groupWeb3: any, member: string, txOptions: any = {}) => { + const group = (await groupWeb3.eth.getAccounts())[0] await unlockAccount(group, groupWeb3) const tx = validators.methods.removeMember(member) let gas = txOptions.gas @@ -118,7 +128,8 @@ describe('governance tests', () => { return tx.send({ from: group, ...txOptions, gas }) } - const addMember = async (groupWeb3: any, group: string, member: string, txOptions: any = {}) => { + const addMember = async (groupWeb3: any, member: string, txOptions: any = {}) => { + const group = (await groupWeb3.eth.getAccounts())[0] await unlockAccount(group, groupWeb3) const tx = validators.methods.addMember(member) let gas = txOptions.gas @@ -128,6 +139,37 @@ describe('governance tests', () => { return tx.send({ from: group, ...txOptions, gas }) } + const authorizeValidatorSigner = async (validatorWeb3: any, signerWeb3: any) => { + const validator: string = (await validatorWeb3.eth.getAccounts())[0] + const signer: string = (await signerWeb3.eth.getAccounts())[0] + await unlockAccount(validator, validatorWeb3) + await unlockAccount(signer, signerWeb3) + const pop = await (await newKitFromWeb3( + signerWeb3 + ).contracts.getAccounts()).generateProofOfSigningKeyPossession(validator, signer) + const accountsWrapper = await newKitFromWeb3(validatorWeb3).contracts.getAccounts() + return (await accountsWrapper.authorizeValidatorSigner(signer, pop)).sendAndWaitForReceipt({ + from: validator, + }) + } + + const updateValidatorBlsKey = async ( + validatorWeb3: any, + signerWeb3: any, + signerPrivateKey: string + ) => { + const validator: string = (await validatorWeb3.eth.getAccounts())[0] + const signer: string = (await signerWeb3.eth.getAccounts())[0] + await unlockAccount(signer, signerWeb3) + const blsPublicKey = getBlsPublicKey(signerPrivateKey) + const blsPop = getBlsPoP(validator, signerPrivateKey) + // TODO(asa): Send this from the signer instead. + const validatorsWrapper = await newKitFromWeb3(validatorWeb3).contracts.getValidators() + return validatorsWrapper + .updateBlsPublicKey(blsPublicKey, blsPop) + .sendAndWaitForReceipt({ from: validator }) + } + const isLastBlockOfEpoch = (blockNumber: number, epochSize: number) => { return blockNumber % epochSize === 0 } @@ -144,32 +186,72 @@ describe('governance tests', () => { const previousBalance = new BigNumber( await token.methods.balanceOf(address).call({}, blockNumber - 1) ) - assert.isNotNaN(currentBalance) - assert.isNotNaN(previousBalance) + assert.isFalse(currentBalance.isNaN()) + assert.isFalse(previousBalance.isNaN()) assertAlmostEqual(currentBalance.minus(previousBalance), expected) } describe('when the validator set is changing', () => { let epoch: number const blockNumbers: number[] = [] - let allValidators: string[] + let validatorAccounts: string[] before(async function(this: any) { this.timeout(0) // Disable test timeout await restart() - const [groupAddress, groupPrivateKey] = await getValidatorGroupKeys() - - const groupInstance = { - name: 'validatorGroup', - validating: false, - syncmode: 'full', - port: 30325, - wsport: 8567, - privateKey: groupPrivateKey.slice(2), - peers: [await getEnode(8545)], - } - await initAndStartGeth(context.hooks.gethBinaryPath, groupInstance) - allValidators = await getValidatorGroupMembers() - assert.equal(allValidators.length, 5) + const groupPrivateKey = await getValidatorGroupPrivateKey() + const rotation0PrivateKey = + '0xa42ac9c99f6ab2c96ee6cae1b40d36187f65cd878737f6623cd363fb94ba7087' + const rotation1PrivateKey = + '0x4519cae145fb9499358be484ca60c80d8f5b7f9c13ff82c88ec9e13283e9de1a' + const additionalNodes: GethInstanceConfig[] = [ + { + name: 'validatorGroup', + validating: false, + syncmode: 'full', + port: 30313, + wsport: 8555, + rpcport: 8557, + privateKey: groupPrivateKey.slice(2), + peers: [await getEnode(8545)], + }, + ] + await Promise.all( + additionalNodes.map((nodeConfig) => + initAndStartGeth(context.hooks.gethBinaryPath, nodeConfig) + ) + ) + // Connect the validating nodes to the non-validating nodes, to test that announce messages + // are properly gossiped. + const additionalValidatingNodes = [ + { + name: 'validator2KeyRotation0', + validating: true, + syncmode: 'full', + lightserv: false, + port: 30315, + wsport: 8559, + privateKey: rotation0PrivateKey.slice(2), + peers: [await getEnode(8557)], + }, + { + name: 'validator2KeyRotation1', + validating: true, + syncmode: 'full', + lightserv: false, + port: 30317, + wsport: 8561, + privateKey: rotation1PrivateKey.slice(2), + peers: [await getEnode(8557)], + }, + ] + await Promise.all( + additionalValidatingNodes.map((nodeConfig) => + initAndStartGeth(context.hooks.gethBinaryPath, nodeConfig) + ) + ) + + validatorAccounts = await getValidatorGroupMembers() + assert.equal(validatorAccounts.length, 5) epoch = new BigNumber(await validators.methods.getEpochSize().call()).toNumber() assert.equal(epoch, 10) @@ -180,26 +262,59 @@ describe('governance tests', () => { await sleep(0.1) } while (blockNumber % epoch !== 1) - await activate(allValidators[0]) - const groupWeb3 = new Web3('ws://localhost:8567') + await activate(validatorAccounts[0]) + + // Prepare for member swapping. + const groupWeb3 = new Web3('ws://localhost:8555') const groupKit = newKitFromWeb3(groupWeb3) validators = await groupKit._web3Contracts.getValidators() - const membersToSwap = [allValidators[0], allValidators[1]] - let includedMemberIndex = 1 - await removeMember(groupWeb3, groupAddress, membersToSwap[0]) + const membersToSwap = [validatorAccounts[0], validatorAccounts[1]] + await removeMember(groupWeb3, membersToSwap[1]) + + // Prepare for key rotation. + const validatorWeb3 = new Web3('http://localhost:8549') + const authorizedWeb3s = [new Web3('ws://localhost:8559'), new Web3('ws://localhost:8561')] + const authorizedPrivateKeys = [rotation0PrivateKey, rotation1PrivateKey] + + let index = 0 + let errorWhileChangingValidatorSet = '' + // Can't recycle signing keys. + let doneAuthorizing = false const changeValidatorSet = async (header: any) => { - blockNumbers.push(header.number) - // At the start of epoch N, swap members so the validator set is different for epoch N + 1. - if (header.number % epoch === 1) { - const memberToRemove = membersToSwap[includedMemberIndex] - const memberToAdd = membersToSwap[(includedMemberIndex + 1) % 2] - await removeMember(groupWeb3, groupAddress, memberToRemove) - await addMember(groupWeb3, groupAddress, memberToAdd) - includedMemberIndex = (includedMemberIndex + 1) % 2 - const newMembers = await getValidatorGroupMembers() - assert.include(newMembers, memberToAdd) - assert.notInclude(newMembers, memberToRemove) + try { + blockNumbers.push(header.number) + // At the start of epoch N, perform actions so the validator set is different for epoch N + 1. + if (header.number % epoch === 1) { + // 1. Swap validator0 and validator1 so one is a member of the group and the other is not. + const memberToRemove = membersToSwap[index] + const memberToAdd = membersToSwap[(index + 1) % 2] + await removeMember(groupWeb3, memberToRemove) + await addMember(groupWeb3, memberToAdd) + const newMembers = await getValidatorGroupMembers() + assert.include(newMembers, memberToAdd) + assert.notInclude(newMembers, memberToRemove) + // 2. Rotate keys for validator 2 by authorizing a new validating key. + if (!doneAuthorizing) { + await authorizeValidatorSigner(validatorWeb3, authorizedWeb3s[index]) + await updateValidatorBlsKey( + validatorWeb3, + authorizedWeb3s[index], + authorizedPrivateKeys[index] + ) + } + doneAuthorizing = doneAuthorizing || index === 1 + const signingKeys = await Promise.all( + newMembers.map((v: string) => getValidatorSigner(v)) + ) + // Confirm that authorizing signing keys worked. + // @ts-ignore Type does not include `notSameMembers` + assert.notSameMembers(newMembers, signingKeys) + index = (index + 1) % 2 + } + } catch (e) { + console.error(e) + errorWhileChangingValidatorSet = e } } @@ -210,12 +325,22 @@ describe('governance tests', () => { ;(subscription as any).unsubscribe() // Wait for the current epoch to complete. await sleep(epoch) + assert.equal(errorWhileChangingValidatorSet, '') }) - const getValidatorSetAtBlock = async (blockNumber: number): Promise => { + const getValidatorSetSignersAtBlock = async (blockNumber: number): Promise => { return election.methods.currentValidators().call({}, blockNumber) } + const getValidatorSetAccountsAtBlock = async (blockNumber: number) => { + const signingKeys = await getValidatorSetSignersAtBlock(blockNumber) + return Promise.all( + signingKeys.map((address: string) => + accounts.methods.signerToAccount(address).call({}, blockNumber) + ) + ) + } + const getLastEpochBlock = (blockNumber: number) => { const epochNumber = Math.floor((blockNumber - 1) / epoch) return epochNumber * epoch @@ -232,20 +357,39 @@ describe('governance tests', () => { } }) - it('should always return a validator set equal to the group members at the end of the last epoch', async () => { + it('should always return a validator set equal to the signing keys of the group members at the end of the last epoch', async function(this: any) { + this.timeout(0) for (const blockNumber of blockNumbers) { const lastEpochBlock = getLastEpochBlock(blockNumber) - const groupMembership = await getValidatorGroupMembers(lastEpochBlock) - const validatorSet = await getValidatorSetAtBlock(blockNumber) - assert.sameMembers(groupMembership, validatorSet) + const memberAccounts = await getValidatorGroupMembers(lastEpochBlock) + const memberSigners = await Promise.all( + memberAccounts.map((v: string) => getValidatorSigner(v, lastEpochBlock)) + ) + const validatorSetSigners = await getValidatorSetSignersAtBlock(blockNumber) + const validatorSetAccounts = await getValidatorSetAccountsAtBlock(blockNumber) + assert.sameMembers(memberSigners, validatorSetSigners) + assert.sameMembers(memberAccounts, validatorSetAccounts) } }) - it('should only have created blocks whose miner was in the current validator set', async () => { + it('should block propose in a round robin fashion', async () => { + let roundRobinOrder: string[] = [] for (const blockNumber of blockNumbers) { - const validatorSet = await getValidatorSetAtBlock(blockNumber) + const lastEpochBlock = getLastEpochBlock(blockNumber) + // Fetch the round robin order if it hasn't already been set for this epoch. + if (roundRobinOrder.length === 0 || blockNumber === lastEpochBlock + 1) { + const validatorSet = await getValidatorSetSignersAtBlock(blockNumber) + roundRobinOrder = await Promise.all( + validatorSet.map( + async (_, i) => (await web3.eth.getBlock(lastEpochBlock + i + 1)).miner + ) + ) + assert.sameMembers(roundRobinOrder, validatorSet) + } + const indexInEpoch = blockNumber - lastEpochBlock - 1 + const expectedProposer = roundRobinOrder[indexInEpoch % roundRobinOrder.length] const block = await web3.eth.getBlock(blockNumber) - assert.include(validatorSet.map((x) => x.toLowerCase()), block.miner.toLowerCase()) + assert.equal(block.miner.toLowerCase(), expectedProposer.toLowerCase()) } }) @@ -257,10 +401,10 @@ describe('governance tests', () => { const assertScoreUnchanged = async (validator: string, blockNumber: number) => { const score = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber))[2] + (await validators.methods.getValidator(validator).call({}, blockNumber)).score ) const previousScore = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber - 1))[2] + (await validators.methods.getValidator(validator).call({}, blockNumber - 1)).score ) assert.isFalse(score.isNaN()) assert.isFalse(previousScore.isNaN()) @@ -269,10 +413,10 @@ describe('governance tests', () => { const assertScoreChanged = async (validator: string, blockNumber: number) => { const score = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber))[2] + (await validators.methods.getValidator(validator).call({}, blockNumber)).score ) const previousScore = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber - 1))[2] + (await validators.methods.getValidator(validator).call({}, blockNumber - 1)).score ) assert.isFalse(score.isNaN()) assert.isFalse(previousScore.isNaN()) @@ -286,10 +430,10 @@ describe('governance tests', () => { let expectUnchangedScores: string[] let expectChangedScores: string[] if (isLastBlockOfEpoch(blockNumber, epoch)) { - expectChangedScores = await getValidatorSetAtBlock(blockNumber) - expectUnchangedScores = allValidators.filter((x) => !expectChangedScores.includes(x)) + expectChangedScores = await getValidatorSetAccountsAtBlock(blockNumber) + expectUnchangedScores = validatorAccounts.filter((x) => !expectChangedScores.includes(x)) } else { - expectUnchangedScores = allValidators + expectUnchangedScores = validatorAccounts expectChangedScores = [] } @@ -316,9 +460,9 @@ describe('governance tests', () => { const getExpectedTotalPayment = async (validator: string, blockNumber: number) => { const score = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber))[2] + (await validators.methods.getValidator(validator).call({}, blockNumber)).score ) - assert.isNotNaN(score) + assert.isFalse(score.isNaN()) // We need to calculate the rewards multiplier for the previous block, before // the rewards actually are awarded. const rewardsMultiplier = new BigNumber( @@ -333,10 +477,12 @@ describe('governance tests', () => { let expectUnchangedBalances: string[] let expectChangedBalances: string[] if (isLastBlockOfEpoch(blockNumber, epoch)) { - expectChangedBalances = await getValidatorSetAtBlock(blockNumber) - expectUnchangedBalances = allValidators.filter((x) => !expectChangedBalances.includes(x)) + expectChangedBalances = await getValidatorSetAccountsAtBlock(blockNumber) + expectUnchangedBalances = validatorAccounts.filter( + (x) => !expectChangedBalances.includes(x) + ) } else { - expectUnchangedBalances = allValidators + expectUnchangedBalances = validatorAccounts expectChangedBalances = [] } @@ -484,13 +630,13 @@ describe('governance tests', () => { ) const difference = currentTarget.minus(previousTarget) - // Assert equal to 10 decimal places due to rounding errors. + // Assert equal to 9 decimal places due to rounding errors. assert.equal( fromFixed(difference) - .dp(10) + .dp(9) .toFixed(), fromFixed(expected) - .dp(10) + .dp(9) .toFixed() ) } diff --git a/packages/celotool/src/e2e-tests/utils.ts b/packages/celotool/src/e2e-tests/utils.ts index 98f669af9e8..9aaaa882178 100644 --- a/packages/celotool/src/e2e-tests/utils.ts +++ b/packages/celotool/src/e2e-tests/utils.ts @@ -213,7 +213,7 @@ export async function init(gethBinaryPath: string, datadir: string, genesisPath: } export async function importPrivateKey(gethBinaryPath: string, instance: GethInstanceConfig) { - const keyFile = '/tmp/key.txt' + const keyFile = `/${getDatadir(instance)}/key.txt` fs.writeFileSync(keyFile, instance.privateKey) console.info(`geth:${instance.name}: import account`) await execCmdWithExitOnFailure( diff --git a/packages/cli/package.json b/packages/cli/package.json index 371e03eab0c..3e7444c9503 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -38,6 +38,7 @@ "@oclif/plugin-help": "^2", "bip32": "^1.0.2", "bip39": "^2.5.0", + "bls12377js": "https://github.com/celo-org/bls12377js#cada1105f4a5e4c2ddd239c1874df3bf33144a10", "chalk": "^2.4.2", "cli-table": "^0.3.1", "cli-ux": "^5.3.1", diff --git a/packages/cli/src/commands/account/authorize.test.ts b/packages/cli/src/commands/account/authorize.test.ts index 1127bd6afcf..fada8a235c7 100644 --- a/packages/cli/src/commands/account/authorize.test.ts +++ b/packages/cli/src/commands/account/authorize.test.ts @@ -8,14 +8,32 @@ process.env.NO_SYNCCHECK = 'true' testWithGanache('account:authorize cmd', (web3: Web3) => { test('can authorize account', async () => { const accounts = await web3.eth.getAccounts() - await Register.run(['--from', accounts[0], '--name', 'Chapulin Colorado']) - await Authorize.run(['--from', accounts[0], '--role', 'validation', '--to', accounts[1]]) + await Register.run(['--from', accounts[0]]) + await Authorize.run([ + '--from', + accounts[0], + '--role', + 'validator', + '--signer', + accounts[1], + '--pop', + '0x1b9fca4bbb5bfb1dbe69ef1cddbd9b4202dcb6b134c5170611e1e36ecfa468d7b46c85328d504934fce6c2a1571603a50ae224d2b32685e84d4d1a1eebad8452eb', + ]) }) test('fails if from is not an account', async () => { const accounts = await web3.eth.getAccounts() await expect( - Authorize.run(['--from', accounts[0], '--role', 'validation', '--to', accounts[1]]) + Authorize.run([ + '--from', + accounts[0], + '--role', + 'validator', + '--signer', + accounts[1], + '--pop', + '0x1b9fca4bbb5bfb1dbe69ef1cddbd9b4202dcb6b134c5170611e1e36ecfa468d7b46c85328d504934fce6c2a1571603a50ae224d2b32685e84d4d1a1eebad8452eb', + ]) ).rejects.toThrow() }) }) diff --git a/packages/cli/src/commands/account/authorize.ts b/packages/cli/src/commands/account/authorize.ts index f34239dae7c..4878f7e4013 100644 --- a/packages/cli/src/commands/account/authorize.ts +++ b/packages/cli/src/commands/account/authorize.ts @@ -5,40 +5,35 @@ import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' export default class Authorize extends BaseCommand { - static description = 'Authorize an attestation, validation or vote signing key' + static description = 'Authorize an attestation, validator, or vote signer' static flags = { ...BaseCommand.flags, from: Flags.address({ required: true }), role: flags.string({ char: 'r', - options: ['vote', 'validation', 'attestation'], + options: ['vote', 'validator', 'attestation'], description: 'Role to delegate', + required: true, }), - to: Flags.address({ required: true }), + pop: flags.string({ + description: 'Proof-of-possession of the signer key', + required: true, + }), + signer: Flags.address({ required: true }), } static args = [] static examples = [ - 'authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role vote --to 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', + 'authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role vote --signer 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb --pop 0x1b9fca4bbb5bfb1dbe69ef1cddbd9b4202dcb6b134c5170611e1e36ecfa468d7b46c85328d504934fce6c2a1571603a50ae224d2b32685e84d4d1a1eebad8452eb', ] async run() { const res = this.parse(Authorize) - - if (!res.flags.role) { - this.error(`Specify role with --role`) - return - } - - if (!res.flags.to) { - this.error(`Specify authorized address with --to`) - return - } - this.kit.defaultAccount = res.flags.from const accounts = await this.kit.contracts.getAccounts() + const sig = accounts.parseSignatureOfAddress(res.flags.from, res.flags.signer, res.flags.pop) await newCheckBuilder(this) .isAccount(res.flags.from) @@ -46,11 +41,11 @@ export default class Authorize extends BaseCommand { let tx: any if (res.flags.role === 'vote') { - tx = await accounts.authorizeVoteSigner(res.flags.from, res.flags.to) - } else if (res.flags.role === 'validation') { - tx = await accounts.authorizeValidationSigner(res.flags.from, res.flags.to) + tx = await accounts.authorizeVoteSigner(res.flags.signer, sig) + } else if (res.flags.role === 'validator') { + tx = await accounts.authorizeValidatorSigner(res.flags.signer, sig) } else if (res.flags.role === 'attestation') { - tx = await accounts.authorizeAttestationSigner(res.flags.from, res.flags.to) + tx = await accounts.authorizeAttestationSigner(res.flags.signer, sig) } else { this.error(`Invalid role provided`) return diff --git a/packages/cli/src/commands/account/claims.test.ts b/packages/cli/src/commands/account/claims.test.ts index df2994703b3..6cb91fb8ce9 100644 --- a/packages/cli/src/commands/account/claims.test.ts +++ b/packages/cli/src/commands/account/claims.test.ts @@ -11,7 +11,7 @@ import CreateMetadata from './create-metadata' import RegisterMetadata from './register-metadata' process.env.NO_SYNCCHECK = 'true' -testWithGanache('account:authorize cmd', (web3: Web3) => { +testWithGanache('account metadata cmds', (web3: Web3) => { let account: string let accounts: string[] beforeEach(async () => { diff --git a/packages/cli/src/commands/account/proof-of-possession.ts b/packages/cli/src/commands/account/proof-of-possession.ts new file mode 100644 index 00000000000..24b0ab206ba --- /dev/null +++ b/packages/cli/src/commands/account/proof-of-possession.ts @@ -0,0 +1,28 @@ +import { serializeSignature } from '@celo/utils/lib/signatureUtils' +import { BaseCommand } from '../../base' +import { printValueMap } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class ProofOfPossession extends BaseCommand { + static description = 'Generate proof-of-possession to be used to authorize a signer' + + static flags = { + ...BaseCommand.flags, + signer: Flags.address({ required: true }), + account: Flags.address({ required: true }), + } + + static examples = [ + 'proof-of-possession --account 0x5409ed021d9299bf6814279a6a1411a7e866a631 --signer 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb', + ] + + async run() { + const res = this.parse(ProofOfPossession) + const accounts = await this.kit.contracts.getAccounts() + const pop = await accounts.generateProofOfSigningKeyPossession( + res.flags.account, + res.flags.signer + ) + printValueMap({ signature: serializeSignature(pop) }) + } +} diff --git a/packages/cli/src/commands/account/register.ts b/packages/cli/src/commands/account/register.ts index ea121e9cdfc..9dd445c5838 100644 --- a/packages/cli/src/commands/account/register.ts +++ b/packages/cli/src/commands/account/register.ts @@ -9,13 +9,16 @@ export default class Register extends BaseCommand { static flags = { ...BaseCommand.flags, - name: flags.string({ required: true }), + name: flags.string(), from: Flags.address({ required: true }), } static args = [] - static examples = ['register'] + static examples = [ + 'register --from 0x5409ed021d9299bf6814279a6a1411a7e866a631', + 'register --from 0x5409ed021d9299bf6814279a6a1411a7e866a631 --name test-account', + ] async run() { const res = this.parse(Register) @@ -26,6 +29,8 @@ export default class Register extends BaseCommand { .isNotAccount(res.flags.from) .runChecks() await displaySendTx('register', accounts.createAccount()) - await displaySendTx('setName', accounts.setName(res.flags.name)) + if (res.flags.name) { + await displaySendTx('setName', accounts.setName(res.flags.name)) + } } } diff --git a/packages/cli/src/commands/validator/publicKey.ts b/packages/cli/src/commands/validator/publicKey.ts deleted file mode 100644 index cfe11ab125a..00000000000 --- a/packages/cli/src/commands/validator/publicKey.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { BaseCommand } from '../../base' -import { newCheckBuilder } from '../../utils/checks' -import { displaySendTx } from '../../utils/cli' -import { Flags } from '../../utils/command' -import { getPubKeyFromAddrAndWeb3 } from '../../utils/helpers' - -export default class ValidatorPublicKey extends BaseCommand { - static description = 'Manage BLS public key data for a validator' - - static flags = { - ...BaseCommand.flags, - from: Flags.address({ required: true, description: "Validator's address" }), - publicKey: Flags.publicKey({ required: true }), - } - - static examples = [ - 'publickey --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', - ] - async run() { - const res = this.parse(ValidatorPublicKey) - this.kit.defaultAccount = res.flags.from - const validators = await this.kit.contracts.getValidators() - const accounts = await this.kit.contracts.getAccounts() - - await newCheckBuilder(this, res.flags.from) - .isSignerOrAccount() - .canSignValidatorTxs() - .signerAccountIsValidator() - .runChecks() - - await displaySendTx( - 'updatePublicKeysData', - validators.updatePublicKeysData(res.flags.publicKey as any) - ) - - // register encryption key on accounts contract - // TODO: Use a different key data encryption - const pubKey = await getPubKeyFromAddrAndWeb3(res.flags.from, this.web3) - // TODO fix typing - const setKeyTx = accounts.setAccountDataEncryptionKey(pubKey as any) - await displaySendTx('Set encryption key', setKeyTx) - } -} diff --git a/packages/cli/src/commands/validator/register.ts b/packages/cli/src/commands/validator/register.ts index bc50dd33dd6..0753ae27088 100644 --- a/packages/cli/src/commands/validator/register.ts +++ b/packages/cli/src/commands/validator/register.ts @@ -1,8 +1,8 @@ +import { addressToPublicKey } from '@celo/utils/lib/signatureUtils' import { BaseCommand } from '../../base' import { newCheckBuilder } from '../../utils/checks' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' -import { getPubKeyFromAddrAndWeb3 } from '../../utils/helpers' export default class ValidatorRegister extends BaseCommand { static description = 'Register a new Validator' @@ -10,12 +10,15 @@ export default class ValidatorRegister extends BaseCommand { static flags = { ...BaseCommand.flags, from: Flags.address({ required: true, description: 'Address for the Validator' }), - publicKey: Flags.publicKey({ required: true }), + ecdsaKey: Flags.ecdsaPublicKey({ required: true }), + blsKey: Flags.blsPublicKey({ required: true }), + blsPop: Flags.blsProofOfPossession({ required: true }), } static examples = [ - 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', + 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --ecdsaKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae1 --blsKey 0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae300 --blsPop 0x05d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', ] + async run() { const res = this.parse(ValidatorRegister) this.kit.defaultAccount = res.flags.from @@ -31,12 +34,16 @@ export default class ValidatorRegister extends BaseCommand { await displaySendTx( 'registerValidator', - validators.registerValidator(res.flags.publicKey as any) + validators.registerValidator( + res.flags.ecdsaKey as any, + res.flags.blsKey as any, + res.flags.blsPop as any + ) ) // register encryption key on accounts contract // TODO: Use a different key data encryption - const pubKey = await getPubKeyFromAddrAndWeb3(res.flags.from, this.web3) + const pubKey = await addressToPublicKey(res.flags.from, this.web3.eth.sign) // TODO fix typing const setKeyTx = accounts.setAccountDataEncryptionKey(pubKey as any) await displaySendTx('Set encryption key', setKeyTx) diff --git a/packages/cli/src/commands/validator/update-bls-public-key.ts b/packages/cli/src/commands/validator/update-bls-public-key.ts new file mode 100644 index 00000000000..70521743dea --- /dev/null +++ b/packages/cli/src/commands/validator/update-bls-public-key.ts @@ -0,0 +1,34 @@ +import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class ValidatorUpdateBlsPublicKey extends BaseCommand { + static description = 'Update BLS key for a validator' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: "Validator's address" }), + blsKey: Flags.blsPublicKey({ required: true }), + blsPop: Flags.blsProofOfPossession({ required: true }), + } + + static examples = [ + 'update-bls-key --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --blsKey 0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae300 --blsPop 0x05d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', + ] + async run() { + const res = this.parse(ValidatorUpdateBlsPublicKey) + this.kit.defaultAccount = res.flags.from + const validators = await this.kit.contracts.getValidators() + await newCheckBuilder(this, res.flags.from) + .isSignerOrAccount() + .canSignValidatorTxs() + .signerAccountIsValidator() + .runChecks() + + await displaySendTx( + 'updateBlsPublicKey', + validators.updateBlsPublicKey(res.flags.blsKey as any, res.flags.blsPop as any) + ) + } +} diff --git a/packages/cli/src/utils/checks.ts b/packages/cli/src/utils/checks.ts index 0e79ab09874..d51d3b21df4 100644 --- a/packages/cli/src/utils/checks.ts +++ b/packages/cli/src/utils/checks.ts @@ -73,7 +73,7 @@ class CheckBuilder { 'Signer can sign Validator Txs', this.withAccounts((lg) => lg - .activeValidationSignerToAccount(this.signer!) + .validatorSignerToAccount(this.signer!) .then(() => true) .catch(() => false) ) diff --git a/packages/cli/src/utils/command.ts b/packages/cli/src/utils/command.ts index dee09ae0690..7268010b70c 100644 --- a/packages/cli/src/utils/command.ts +++ b/packages/cli/src/utils/command.ts @@ -1,3 +1,4 @@ +import { BLS_POP_SIZE, BLS_PUBLIC_KEY_SIZE } from '@celo/utils/lib/bls' import { URL_REGEX } from '@celo/utils/lib/io' import { flags } from '@oclif/command' import { CLIError } from '@oclif/errors' @@ -5,14 +6,24 @@ import { IArg, ParseFn } from '@oclif/parser/lib/args' import { pathExistsSync } from 'fs-extra' import Web3 from 'web3' -const parsePublicKey: ParseFn = (input) => { - // Check that the string starts with 0x and has byte length of ecdsa pub key (64 bytes) + bls pub key (48 bytes) + proof of pos (96 bytes) - if (Web3.utils.isHex(input) && input.length === 418 && input.startsWith('0x')) { +const parseBytes = (input: string, length: number, msg: string) => { + // Check that the string starts with 0x and has byte length of `length`. + if (Web3.utils.isHex(input) && input.length === length && input.startsWith('0x')) { return input } else { - throw new CLIError(`${input} is not a public key`) + throw new CLIError(msg) } } + +const parseEcdsaPublicKey: ParseFn = (input) => { + return parseBytes(input, 64, `${input} is not an ECDSA public key`) +} +const parseBlsPublicKey: ParseFn = (input) => { + return parseBytes(input, BLS_PUBLIC_KEY_SIZE, `${input} is not a BLS public key`) +} +const parseBlsProofOfPossession: ParseFn = (input) => { + return parseBytes(input, BLS_POP_SIZE, `${input} is not a BLS proof-of-possession`) +} const parseAddress: ParseFn = (input) => { if (Web3.utils.isAddress(input)) { return input @@ -54,9 +65,19 @@ export const Flags = { description: 'Account Address', helpValue: '0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', }), - publicKey: flags.build({ - parse: parsePublicKey, - description: 'Public Key', + ecdsaPublicKey: flags.build({ + parse: parseEcdsaPublicKey, + description: 'ECDSA Public Key', + helpValue: '0x', + }), + blsPublicKey: flags.build({ + parse: parseBlsPublicKey, + description: 'BLS Public Key', + helpValue: '0x', + }), + blsProofOfPossession: flags.build({ + parse: parseBlsProofOfPossession, + description: 'BLS Proof-of-Possession', helpValue: '0x', }), url: flags.build({ diff --git a/packages/cli/src/utils/helpers.ts b/packages/cli/src/utils/helpers.ts index d95c1354c57..4d8c7c0f768 100644 --- a/packages/cli/src/utils/helpers.ts +++ b/packages/cli/src/utils/helpers.ts @@ -1,29 +1,7 @@ -import { eqAddress } from '@celo/utils/lib/address' -import ethjsutil from 'ethereumjs-util' import Web3 from 'web3' import { Block } from 'web3/eth/types' import { failWith } from './cli' -import assert = require('assert') - -export async function getPubKeyFromAddrAndWeb3(addr: string, web3: Web3) { - const msg = new Buffer('dummy_msg_data') - const data = '0x' + msg.toString('hex') - // Note: Eth.sign typing displays incorrect parameter order - const sig = await web3.eth.sign(data, addr) - - const rawsig = ethjsutil.fromRpcSig(sig) - - const prefix = new Buffer('\x19Ethereum Signed Message:\n') - const prefixedMsg = ethjsutil.sha3(Buffer.concat([prefix, new Buffer(String(msg.length)), msg])) - const pubKey = ethjsutil.ecrecover(prefixedMsg, rawsig.v, rawsig.r, rawsig.s) - - const computedAddr = ethjsutil.pubToAddress(pubKey).toString('hex') - assert(eqAddress(computedAddr, addr), 'computed address !== addr') - - return pubKey -} - export async function nodeIsSynced(web3: Web3): Promise { if (process.env.NO_SYNCCHECK) { return true diff --git a/packages/contractkit/src/wrappers/Accounts.test.ts b/packages/contractkit/src/wrappers/Accounts.test.ts new file mode 100644 index 00000000000..95bd22d4a18 --- /dev/null +++ b/packages/contractkit/src/wrappers/Accounts.test.ts @@ -0,0 +1,74 @@ +import { addressToPublicKey, parseSignature } from '@celo/utils/lib/signatureUtils' +import Web3 from 'web3' +import { newKitFromWeb3 } from '../kit' +import { testWithGanache } from '../test-utils/ganache-test' +import { AccountsWrapper } from './Accounts' +import { LockedGoldWrapper } from './LockedGold' +import { ValidatorsWrapper } from './Validators' + +/* +TEST NOTES: +- In migrations: The only account that has cUSD is accounts[0] +*/ + +const minLockedGoldValue = Web3.utils.toWei('10', 'ether') // 10 gold + +// Random hex strings +const blsPublicKey = + '0x4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' +const blsPoP = + '0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' + +testWithGanache('Accounts Wrapper', (web3) => { + const kit = newKitFromWeb3(web3) + let accounts: string[] = [] + let accountsInstance: AccountsWrapper + let validators: ValidatorsWrapper + let lockedGold: LockedGoldWrapper + + const registerAccountWithLockedGold = async (account: string) => { + if (!(await accountsInstance.isAccount(account))) { + await accountsInstance.createAccount().sendAndWaitForReceipt({ from: account }) + } + await lockedGold.lock().sendAndWaitForReceipt({ from: account, value: minLockedGoldValue }) + } + + const getParsedSignatureOfAddress = async (address: string, signer: string) => { + const addressHash = web3.utils.soliditySha3({ type: 'address', value: address }) + const signature = await web3.eth.sign(addressHash, signer) + return parseSignature(addressHash, signature, signer) + } + + beforeAll(async () => { + accounts = await web3.eth.getAccounts() + validators = await kit.contracts.getValidators() + lockedGold = await kit.contracts.getLockedGold() + accountsInstance = await kit.contracts.getAccounts() + }) + + const setupValidator = async (validatorAccount: string) => { + const publicKey = await addressToPublicKey(validatorAccount, web3.eth.sign) + await registerAccountWithLockedGold(validatorAccount) + await validators + // @ts-ignore + .registerValidator(publicKey, blsPublicKey, blsPoP) + .sendAndWaitForReceipt({ from: validatorAccount }) + } + + test('SBAT authorize validator key when not a validator', async () => { + const account = accounts[0] + const signer = accounts[1] + await accountsInstance.createAccount() + const sig = await getParsedSignatureOfAddress(account, signer) + await accountsInstance.authorizeValidatorSigner(signer, sig) + }) + + test('SBAT authorize validator key when a validator', async () => { + const account = accounts[0] + const signer = accounts[1] + await accountsInstance.createAccount() + await setupValidator(account) + const sig = await getParsedSignatureOfAddress(account, signer) + await accountsInstance.authorizeValidatorSigner(signer, sig) + }) +}) diff --git a/packages/contractkit/src/wrappers/Accounts.ts b/packages/contractkit/src/wrappers/Accounts.ts index 01afb340612..a995bc664e6 100644 --- a/packages/contractkit/src/wrappers/Accounts.ts +++ b/packages/contractkit/src/wrappers/Accounts.ts @@ -1,3 +1,9 @@ +import { + hashMessageWithPrefix, + parseSignature, + Signature, + signedMessageToPublicKey, +} from '@celo/utils/lib/signatureUtils' import Web3 from 'web3' import { Address } from '../base' import { Accounts } from '../generated/types/Accounts' @@ -9,11 +15,6 @@ import { toTransactionObject, } from '../wrappers/BaseWrapper' -enum SignerRole { - Attestation, - Validation, - Vote, -} /** * Contract for handling deposits needed for voting. */ @@ -40,12 +41,12 @@ export class AccountsWrapper extends BaseWrapper { this.contract.methods.getVoteSigner ) /** - * Returns the validation signere for the specified account. + * Returns the validator signer for the specified account. * @param account The address of the account. * @return The address with which the account can register a validator or group. */ - getValidationSigner: (account: string) => Promise
= proxyCall( - this.contract.methods.getValidationSigner + getValidatorSigner: (account: string) => Promise
= proxyCall( + this.contract.methods.getValidatorSigner ) /** @@ -53,8 +54,8 @@ export class AccountsWrapper extends BaseWrapper { * @param signer Address that is authorized to sign the tx as validator * @return The Account address */ - activeValidationSignerToAccount: (signer: Address) => Promise
= proxyCall( - this.contract.methods.activeValidationSignerToAccount + validatorSignerToAccount: (signer: Address) => Promise
= proxyCall( + this.contract.methods.validatorSignerToAccount ) /** @@ -69,44 +70,98 @@ export class AccountsWrapper extends BaseWrapper { * @param address The address of the account * @return Returns `true` if account exists. Returns `false` otherwise. */ - isSigner: (address: string) => Promise = proxyCall(this.contract.methods.isAuthorized) + isSigner: (address: string) => Promise = proxyCall( + this.contract.methods.isAuthorizedSigner + ) /** * Authorize an attestation signing key on behalf of this account to another address. - * @param account Address of the active account. - * @param attestationSigner The address of the signing key to authorize. + * @param signer The address of the signing key to authorize. + * @param proofOfSigningKeyPossession The account address signed by the signer address. * @return A CeloTransactionObject */ async authorizeAttestationSigner( - account: Address, - attestationSigner: Address + signer: Address, + proofOfSigningKeyPossession: Signature ): Promise> { - return this.authorizeSigner(SignerRole.Attestation, account, attestationSigner) + return toTransactionObject( + this.kit, + this.contract.methods.authorizeAttestationSigner( + signer, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + proofOfSigningKeyPossession.s + ) + ) } /** * Authorizes an address to sign votes on behalf of the account. - * @param account Address of the active account. - * @param voteSigner The address of the vote signing key to authorize. + * @param signer The address of the vote signing key to authorize. + * @param proofOfSigningKeyPossession The account address signed by the signer address. * @return A CeloTransactionObject */ async authorizeVoteSigner( - account: Address, - voteSigner: Address + signer: Address, + proofOfSigningKeyPossession: Signature ): Promise> { - return this.authorizeSigner(SignerRole.Vote, account, voteSigner) + return toTransactionObject( + this.kit, + this.contract.methods.authorizeVoteSigner( + signer, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + proofOfSigningKeyPossession.s + ) + ) } /** * Authorizes an address to sign consensus messages on behalf of the account. - * @param account Address of the active account. - * @param validationSigner The address of the signing key to authorize. + * @param signer The address of the signing key to authorize. + * @param proofOfSigningKeyPossession The account address signed by the signer address. * @return A CeloTransactionObject */ - async authorizeValidationSigner( - account: Address, - validationSigner: Address + async authorizeValidatorSigner( + signer: Address, + proofOfSigningKeyPossession: Signature ): Promise> { - return this.authorizeSigner(SignerRole.Validation, account, validationSigner) + const validators = await this.kit.contracts.getValidators() + const account = this.kit.defaultAccount || (await this.kit.web3.eth.getAccounts())[0] + if (await validators.isValidator(account)) { + const message = this.kit.web3.utils.soliditySha3({ type: 'address', value: account }) + const prefixedMsg = hashMessageWithPrefix(message) + const pubKey = signedMessageToPublicKey( + prefixedMsg, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + proofOfSigningKeyPossession.s + ) + return toTransactionObject( + this.kit, + this.contract.methods.authorizeValidatorSigner( + signer, + pubKey, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + // @ts-ignore Typescript does not support overloading. + proofOfSigningKeyPossession.s + ) + ) + } else { + return toTransactionObject( + this.kit, + this.contract.methods.authorizeValidatorSigner( + signer, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + proofOfSigningKeyPossession.s + ) + ) + } + } + + async generateProofOfSigningKeyPossession(account: Address, signer: Address) { + return this.getParsedSignatureOfAddress(account, signer) } /** @@ -168,25 +223,14 @@ export class AccountsWrapper extends BaseWrapper { */ setWalletAddress = proxySend(this.kit, this.contract.methods.setWalletAddress) - private authorizeFns = { - [SignerRole.Attestation]: this.contract.methods.authorizeAttestationSigner, - [SignerRole.Validation]: this.contract.methods.authorizeValidationSigner, - [SignerRole.Vote]: this.contract.methods.authorizeVoteSigner, - } - - private async authorizeSigner(role: SignerRole, account: Address, signer: Address) { - const sig = await this.getParsedSignatureOfAddress(account, signer) - // TODO(asa): Pass default tx "from" argument. - return toTransactionObject(this.kit, this.authorizeFns[role](signer, sig.v, sig.r, sig.s)) + parseSignatureOfAddress(address: Address, signer: string, signature: string) { + const hash = Web3.utils.soliditySha3({ type: 'address', value: address }) + return parseSignature(hash, signature, signer) } private async getParsedSignatureOfAddress(address: Address, signer: string) { const hash = Web3.utils.soliditySha3({ type: 'address', value: address }) - const signature = (await this.kit.web3.eth.sign(hash, signer)).slice(2) - return { - r: `0x${signature.slice(0, 64)}`, - s: `0x${signature.slice(64, 128)}`, - v: Web3.utils.hexToNumber(signature.slice(128, 130)) + 27, - } + const signature = await this.kit.web3.eth.sign(hash, signer) + return parseSignature(hash, signature, signer) } } diff --git a/packages/contractkit/src/wrappers/Validators.test.ts b/packages/contractkit/src/wrappers/Validators.test.ts index c8b0dfcde1c..e90aec9cdfe 100644 --- a/packages/contractkit/src/wrappers/Validators.test.ts +++ b/packages/contractkit/src/wrappers/Validators.test.ts @@ -1,3 +1,4 @@ +import { addressToPublicKey } from '@celo/utils/lib/signatureUtils' import BigNumber from 'bignumber.js' import Web3 from 'web3' import { newKitFromWeb3 } from '../kit' @@ -13,15 +14,10 @@ TEST NOTES: const minLockedGoldValue = Web3.utils.toWei('10', 'ether') // 10 gold -// A random 64 byte hex string. -const publicKey = - 'ea0733ad275e2b9e05541341a97ee82678c58932464fad26164657a111a7e37a9fa0300266fb90e2135a1f1512350cb4e985488a88809b14e3cbe415e76e82b2' const blsPublicKey = - '4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' + '0x4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' const blsPoP = - '9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' - -const publicKeysData = '0x' + publicKey + blsPublicKey + blsPoP + '0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' testWithGanache('Validators Wrapper', (web3) => { const kit = newKitFromWeb3(web3) @@ -52,13 +48,12 @@ testWithGanache('Validators Wrapper', (web3) => { } const setupValidator = async (validatorAccount: string) => { + const publicKey = await addressToPublicKey(validatorAccount, web3.eth.sign) await registerAccountWithLockedGold(validatorAccount) // set account1 as the validator await validators - .registerValidator( - // @ts-ignore - publicKeysData - ) + // @ts-ignore + .registerValidator(publicKey, blsPublicKey, blsPoP) .sendAndWaitForReceipt({ from: validatorAccount }) } diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 548f877d823..0cd4d737365 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -18,7 +18,8 @@ import { export interface Validator { address: Address - publicKey: string + ecdsaPublicKey: string + blsPublicKey: string affiliation: string | null score: BigNumber } @@ -57,7 +58,6 @@ export class ValidatorsWrapper extends BaseWrapper { this.contract.methods.updateCommission(toFixed(commission).toFixed()) ) } - updatePublicKeysData = proxySend(this.kit, this.contract.methods.updatePublicKeysData) /** * Returns the Locked Gold requirements for validators. * @returns The Locked Gold requirements for validators. @@ -100,9 +100,26 @@ export class ValidatorsWrapper extends BaseWrapper { async signerToAccount(signerAddress: Address) { const accounts = await this.kit.contracts.getAccounts() - return accounts.activeValidationSignerToAccount(signerAddress) + return accounts.validatorSignerToAccount(signerAddress) } + /** + * Updates a validator's BLS key. + * @param blsPublicKey The BLS public key that the validator is using for consensus, should pass proof + * of possession. 48 bytes. + * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the + * account address. 96 bytes. + * @return True upon success. + */ + updateBlsPublicKey: ( + blsPublicKey: string, + blsPop: string + ) => CeloTransactionObject = proxySend( + this.kit, + this.contract.methods.updateBlsPublicKey, + tupleParser(parseBytes, parseBytes) + ) + /** * Returns whether a particular account has a registered validator. * @param account The account. @@ -147,9 +164,10 @@ export class ValidatorsWrapper extends BaseWrapper { const res = await this.contract.methods.getValidator(address).call() return { address, - publicKey: res[0] as any, - affiliation: res[1], - score: fromFixed(new BigNumber(res[2])), + ecdsaPublicKey: res[0] as any, + blsPublicKey: res[1] as any, + affiliation: res[2], + score: fromFixed(new BigNumber(res[3])), } } @@ -214,20 +232,23 @@ export class ValidatorsWrapper extends BaseWrapper { * Registers a validator unaffiliated with any validator group. * * Fails if the account is already a validator or validator group. - * Fails if the account does not have sufficient weight. * - * @param publicKeysData Comprised of three tightly-packed elements: - * - publicKey - The public key that the validator is using for consensus, should match - * msg.sender. 64 bytes. - * - blsPublicKey - The BLS public key that the validator is using for consensus, should pass - * proof of possession. 48 bytes. - * - blsPoP - The BLS public key proof of possession. 96 bytes. + * @param ecdsaPublicKey The ECDSA public key that the validator is using for consensus, should match + * the validator signer. 64 bytes. + * @param blsPublicKey The BLS public key that the validator is using for consensus, should pass proof + * of possession. 48 bytes. + * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the + * account address. 96 bytes. */ - registerValidator: (publicKeysData: string) => CeloTransactionObject = proxySend( + registerValidator: ( + ecdsaPublicKey: string, + blsPublicKey: string, + blsPop: string + ) => CeloTransactionObject = proxySend( this.kit, this.contract.methods.registerValidator, - tupleParser(parseBytes) + tupleParser(parseBytes, parseBytes, parseBytes) ) /** diff --git a/packages/docs/command-line-interface/account.md b/packages/docs/command-line-interface/account.md index 91f4cd511cf..c22e3daa1e3 100644 --- a/packages/docs/command-line-interface/account.md +++ b/packages/docs/command-line-interface/account.md @@ -6,20 +6,23 @@ description: Manage your account, send and receive Celo Gold and Celo Dollars ### Authorize -Authorize an attestation, validation or vote signing key +Authorize an attestation, validator, or vote signer ``` USAGE $ celocli account:authorize OPTIONS - -r, --role=vote|validation|attestation Role to delegate - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address - --to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + -r, --role=vote|validator|attestation (required) Role to delegate + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + --pop=pop (required) Proof-of-possession of the signer key + --signer=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address EXAMPLE - authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role vote --to - 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d + authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role vote --signer + 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb --pop + 0x1b9fca4bbb5bfb1dbe69ef1cddbd9b4202dcb6b134c5170611e1e36ecfa468d7b46c85328d504934fce6c2a1571603a50ae224d2b32685e84d4d + 1a1eebad8452eb ``` _See code: [packages/cli/src/commands/account/authorize.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/authorize.ts)_ @@ -229,6 +232,25 @@ EXAMPLE _See code: [packages/cli/src/commands/account/new.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/new.ts)_ +### Proof-of-possession + +Generate proof-of-possession to be used to authorize a signer + +``` +USAGE + $ celocli account:proof-of-possession + +OPTIONS + --account=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + --signer=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + +EXAMPLE + proof-of-possession --account 0x5409ed021d9299bf6814279a6a1411a7e866a631 --signer + 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb +``` + +_See code: [packages/cli/src/commands/account/proof-of-possession.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/proof-of-possession.ts)_ + ### Register Register an account @@ -239,10 +261,11 @@ USAGE OPTIONS --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address - --name=name (required) + --name=name -EXAMPLE - register +EXAMPLES + register --from 0x5409ed021d9299bf6814279a6a1411a7e866a631 + register --from 0x5409ed021d9299bf6814279a6a1411a7e866a631 --name test-account ``` _See code: [packages/cli/src/commands/account/register.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/register.ts)_ diff --git a/packages/docs/command-line-interface/validator.md b/packages/docs/command-line-interface/validator.md index 308a79300f3..8bfdea3e57d 100644 --- a/packages/docs/command-line-interface/validator.md +++ b/packages/docs/command-line-interface/validator.md @@ -72,28 +72,6 @@ EXAMPLE _See code: [packages/cli/src/commands/validator/list.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/list.ts)_ -### PublicKey - -Manage BLS public key data for a validator - -``` -USAGE - $ celocli validator:publicKey - -OPTIONS - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Validator's address - --publicKey=0x (required) Public Key - -EXAMPLE - publickey --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --publicKey - 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf - 997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d - 785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d - 96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00 -``` - -_See code: [packages/cli/src/commands/validator/publicKey.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/publicKey.ts)_ - ### Register Register a new Validator @@ -103,15 +81,18 @@ USAGE $ celocli validator:register OPTIONS + --blsKey=0x (required) BLS Public Key + --blsPop=0x (required) BLS Proof-of-Possession + --ecdsaKey=0x (required) ECDSA Public Key --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address for the Validator - --publicKey=0x (required) Public Key EXAMPLE - register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --publicKey + register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --ecdsaKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf - 997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d - 785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d - 96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00 + 997eda082ae1 --blsKey + 0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae300 --blsPop + 0x05d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26 + dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00 ``` _See code: [packages/cli/src/commands/validator/register.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/register.ts)_ @@ -146,3 +127,25 @@ EXAMPLE ``` _See code: [packages/cli/src/commands/validator/show.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/show.ts)_ + +### Update-bls-public-key + +Update BLS key for a validator + +``` +USAGE + $ celocli validator:update-bls-public-key + +OPTIONS + --blsKey=0x (required) BLS Public Key + --blsPop=0x (required) BLS Proof-of-Possession + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Validator's address + +EXAMPLE + update-bls-key --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --blsKey + 0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae300 --blsPop + 0x05d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26 + dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00 +``` + +_See code: [packages/cli/src/commands/validator/update-bls-public-key.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/update-bls-public-key.ts)_ diff --git a/packages/mobile/src/identity/commentKey.ts b/packages/mobile/src/identity/commentKey.ts index 7dbae492aab..c54deaf486a 100644 --- a/packages/mobile/src/identity/commentKey.ts +++ b/packages/mobile/src/identity/commentKey.ts @@ -1,5 +1,5 @@ +import { stripHexLeader } from '@celo/utils/src/address' import { encryptComment as encryptCommentRaw } from '@celo/utils/src/commentEncryption' -import { stripHexLeader } from '@celo/utils/src/signatureUtils' import { getAttestationsContract, getDataEncryptionKey } from '@celo/walletkit' import { web3 } from 'src/web3/contracts' diff --git a/packages/mobile/src/identity/verification.ts b/packages/mobile/src/identity/verification.ts index 95405fd427d..3f1282a7455 100644 --- a/packages/mobile/src/identity/verification.ts +++ b/packages/mobile/src/identity/verification.ts @@ -1,6 +1,6 @@ +import { eqAddress } from '@celo/utils/src/address' import { compressedPubKey } from '@celo/utils/src/commentEncryption' import { getPhoneHash, isE164Number } from '@celo/utils/src/phoneNumbers' -import { areAddressesEqual } from '@celo/utils/src/signatureUtils' import { ActionableAttestation, extractAttestationCodeFromMessage, @@ -487,10 +487,7 @@ function* setAccount(attestationsContract: AttestationsType, address: string, da Logger.debug(TAG, 'Setting wallet address and public data encryption key') const currentWalletAddress = yield call(getWalletAddress, attestationsContract, address) const currentWalletDEK = yield call(getDataEncryptionKey, attestationsContract, address) - if ( - !areAddressesEqual(currentWalletAddress, address) || - !areAddressesEqual(currentWalletDEK, dataKey) - ) { + if (!eqAddress(currentWalletAddress, address) || !eqAddress(currentWalletDEK, dataKey)) { const setAccountTx = makeSetAccountTx(attestationsContract, address, dataKey) yield call(sendTransaction, setAccountTx, address, TAG, `Set Wallet Address & DEK`) CeloAnalytics.track(CustomEventNames.verification_set_account) diff --git a/packages/mobile/src/import/saga.ts b/packages/mobile/src/import/saga.ts index f3d40c96680..5be02a1c3bf 100644 --- a/packages/mobile/src/import/saga.ts +++ b/packages/mobile/src/import/saga.ts @@ -1,4 +1,4 @@ -import { ensureHexLeader } from '@celo/utils/src/signatureUtils' +import { ensureHexLeader } from '@celo/utils/src/address' import BigNumber from 'bignumber.js' import { validateMnemonic } from 'bip39' import { mnemonicToSeedHex } from 'react-native-bip39' diff --git a/packages/mobile/src/invite/saga.ts b/packages/mobile/src/invite/saga.ts index b19bc5b3d95..e6a16fc9238 100644 --- a/packages/mobile/src/invite/saga.ts +++ b/packages/mobile/src/invite/saga.ts @@ -1,5 +1,5 @@ +import { stripHexLeader } from '@celo/utils/src/address' import { getPhoneHash } from '@celo/utils/src/phoneNumbers' -import { stripHexLeader } from '@celo/utils/src/signatureUtils' import { getEscrowContract, getGoldTokenContract, getStableTokenContract } from '@celo/walletkit' import BigNumber from 'bignumber.js' import { Linking, Platform } from 'react-native' diff --git a/packages/mobile/src/qrcode/utils.ts b/packages/mobile/src/qrcode/utils.ts index a7f62532841..5e8adbc3e05 100644 --- a/packages/mobile/src/qrcode/utils.ts +++ b/packages/mobile/src/qrcode/utils.ts @@ -1,4 +1,4 @@ -import { isValidAddress } from '@celo/utils/src/signatureUtils' +import { isValidAddress } from '@celo/utils/src/address' import { isEmpty } from 'lodash' import * as RNFS from 'react-native-fs' import Share from 'react-native-share' diff --git a/packages/mobile/src/recipients/RecipientPicker.tsx b/packages/mobile/src/recipients/RecipientPicker.tsx index 9c0d72ce22d..b8fddb44e4e 100644 --- a/packages/mobile/src/recipients/RecipientPicker.tsx +++ b/packages/mobile/src/recipients/RecipientPicker.tsx @@ -8,8 +8,8 @@ import ForwardChevron from '@celo/react-components/icons/ForwardChevron' import QRCode from '@celo/react-components/icons/QRCode' import colors from '@celo/react-components/styles/colors' import { fontStyles } from '@celo/react-components/styles/fonts' +import { isValidAddress } from '@celo/utils/src/address' import { parsePhoneNumber } from '@celo/utils/src/phoneNumbers' -import { isValidAddress } from '@celo/utils/src/signatureUtils' import { TranslationFunction } from 'i18next' import * as React from 'react' import { withNamespaces, WithNamespaces } from 'react-i18next' diff --git a/packages/mobile/src/verify/VerificationCodeRow.tsx b/packages/mobile/src/verify/VerificationCodeRow.tsx index 548220d959d..98614ab767f 100644 --- a/packages/mobile/src/verify/VerificationCodeRow.tsx +++ b/packages/mobile/src/verify/VerificationCodeRow.tsx @@ -3,7 +3,7 @@ import withTextInputPasteAware from '@celo/react-components/components/WithTextI import Checkmark from '@celo/react-components/icons/Checkmark' import colors from '@celo/react-components/styles/colors' import fontStyles from '@celo/react-components/styles/fonts' -import { stripHexLeader } from '@celo/utils/src/signatureUtils' +import { stripHexLeader } from '@celo/utils/src/address' import { extractAttestationCodeFromMessage } from '@celo/walletkit' import * as React from 'react' import { withNamespaces, WithNamespaces } from 'react-i18next' diff --git a/packages/protocol/contracts/common/Accounts.sol b/packages/protocol/contracts/common/Accounts.sol index 027eba383e8..9e04877b836 100644 --- a/packages/protocol/contracts/common/Accounts.sol +++ b/packages/protocol/contracts/common/Accounts.sol @@ -1,7 +1,8 @@ pragma solidity ^0.5.3; -import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; +import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; import "./interfaces/IAccounts.sol"; @@ -9,21 +10,21 @@ import "../common/Initializable.sol"; import "../common/Signatures.sol"; import "../common/UsingRegistry.sol"; -contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry { +contract Accounts is IAccounts, Ownable, ReentrancyGuard, Initializable, UsingRegistry { using SafeMath for uint256; struct Signers { //The address that is authorized to vote in governance and validator elections on behalf of the // account. The account can vote as well, whether or not an vote signing key has been specified. - address voting; + address vote; // The address that is authorized to manage a validator or validator group and sign consensus // messages on behalf of the account. The account can manage the validator, whether or not an - // validation signing key has been specified. However if an validation signing key has been + // validator signing key has been specified. However if an validator signing key has been // specified, only that key may actually participate in consensus. - address validating; + address validator; // The address of the key with which this account wants to sign attestations on the Attestations // contract - address attesting; + address attestation; } struct Account { @@ -44,18 +45,23 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry { } mapping(address => Account) private accounts; - // Maps voting and validating keys to the account that provided the authorization. + // Maps authorized signers to the account that provided the authorization. mapping(address => address) public authorizedBy; event AttestationSignerAuthorized(address indexed account, address signer); event VoteSignerAuthorized(address indexed account, address signer); - event ValidationSignerAuthorized(address indexed account, address signer); + event ValidatorSignerAuthorized(address indexed account, address signer); event AccountDataEncryptionKeySet(address indexed account, bytes dataEncryptionKey); event AccountNameSet(address indexed account, string name); event AccountMetadataURLSet(address indexed account, string metadataURL); event AccountWalletAddressSet(address indexed account, address walletAddress); event AccountCreated(address indexed account); + function initialize(address registryAddress) external initializer { + _transferOwnership(msg.sender); + setRegistry(registryAddress); + } + /** * @notice Convenience Setter for the dataEncryptionKey and wallet address for an account * @param name A string to set as the name of the account @@ -73,6 +79,48 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry { setWalletAddress(walletAddress); } + /** + * @notice Creates an account. + * @return True if account creation succeeded. + */ + function createAccount() public returns (bool) { + require(isNotAccount(msg.sender) && isNotAuthorizedSigner(msg.sender)); + Account storage account = accounts[msg.sender]; + account.exists = true; + emit AccountCreated(msg.sender); + return true; + } + + /** + * @notice Setter for the name of an account. + * @param name The name to set. + */ + function setName(string memory name) public { + require(isAccount(msg.sender)); + accounts[msg.sender].name = name; + emit AccountNameSet(msg.sender, name); + } + + /** + * @notice Setter for the wallet address for an account + * @param walletAddress The wallet address to set for the account + */ + function setWalletAddress(address walletAddress) public { + require(isAccount(msg.sender)); + accounts[msg.sender].walletAddress = walletAddress; + emit AccountWalletAddressSet(msg.sender, walletAddress); + } + + /** + * @notice Setter for the data encryption key and version. + * @param dataEncryptionKey secp256k1 public key for data encryption. Preferably compressed. + */ + function setAccountDataEncryptionKey(bytes memory dataEncryptionKey) public { + require(dataEncryptionKey.length >= 33, "data encryption key length <= 32"); + accounts[msg.sender].dataEncryptionKey = dataEncryptionKey; + emit AccountDataEncryptionKeySet(msg.sender, dataEncryptionKey); + } + /** * @notice Setter for the metadata of an account. * @param metadataURL The URL to access the metadata. @@ -85,102 +133,179 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry { /** * @notice Authorizes an address to sign votes on behalf of the account. - * @param voter The address of the vote signing key to authorize. + * @param signer The address of the signing key to authorize. * @param v The recovery id of the incoming ECDSA signature. * @param r Output value r of the ECDSA signature. * @param s Output value s of the ECDSA signature. - * @dev v, r, s constitute `voter`'s signature on `msg.sender`. + * @dev v, r, s constitute `signer`'s signature on `msg.sender`. */ - function authorizeVoteSigner(address voter, uint8 v, bytes32 r, bytes32 s) external nonReentrant { + function authorizeVoteSigner(address signer, uint8 v, bytes32 r, bytes32 s) + external + nonReentrant + { Account storage account = accounts[msg.sender]; - authorize(voter, account.signers.voting, v, r, s); - account.signers.voting = voter; - emit VoteSignerAuthorized(msg.sender, voter); + authorize(signer, v, r, s); + account.signers.vote = signer; + emit VoteSignerAuthorized(msg.sender, signer); } /** * @notice Authorizes an address to sign consensus messages on behalf of the account. - * @param validator The address of the signing key to authorize. + * @param signer The address of the signing key to authorize. * @param v The recovery id of the incoming ECDSA signature. * @param r Output value r of the ECDSA signature. * @param s Output value s of the ECDSA signature. - * @dev v, r, s constitute `validator`'s signature on `msg.sender`. + * @dev v, r, s constitute `signer`'s signature on `msg.sender`. */ - function authorizeValidationSigner(address validator, uint8 v, bytes32 r, bytes32 s) + function authorizeValidatorSigner(address signer, uint8 v, bytes32 r, bytes32 s) external nonReentrant { Account storage account = accounts[msg.sender]; - authorize(validator, account.signers.validating, v, r, s); - account.signers.validating = validator; - emit ValidationSignerAuthorized(msg.sender, validator); + authorize(signer, v, r, s); + account.signers.validator = signer; + require(!getValidators().isValidator(msg.sender)); + emit ValidatorSignerAuthorized(msg.sender, signer); } /** - * @notice Check if an address has been authorized by an account for voting or validating. - * @param account The possibly authorized address. - * @return Returns `true` if authorized. Returns `false` otherwise. + * @notice Authorizes an address to sign consensus messages on behalf of the account. + * @param signer The address of the signing key to authorize. + * @param ecdsaPublicKey The ECDSA public key corresponding to `signer`. + * @param v The recovery id of the incoming ECDSA signature. + * @param r Output value r of the ECDSA signature. + * @param s Output value s of the ECDSA signature. + * @dev v, r, s constitute `signer`'s signature on `msg.sender`. */ - function isAuthorized(address account) external view returns (bool) { - return (authorizedBy[account] != address(0)); + function authorizeValidatorSigner( + address signer, + bytes calldata ecdsaPublicKey, + uint8 v, + bytes32 r, + bytes32 s + ) external nonReentrant { + Account storage account = accounts[msg.sender]; + authorize(signer, v, r, s); + account.signers.validator = signer; + require(getValidators().updateEcdsaPublicKey(msg.sender, signer, ecdsaPublicKey)); + emit ValidatorSignerAuthorized(msg.sender, signer); } /** - * @notice Returns the account associated with `accountOrAttestationSigner`. - * @param accountOrAttestationSigner The address of the account or active authorized attestation - signer. - * @dev Fails if the `accountOrAttestationSigner` is not an account or active authorized - attestation signer. + * @notice Authorizes an address to sign attestations on behalf of the account. + * @param signer The address of the signing key to authorize. + * @param v The recovery id of the incoming ECDSA signature. + * @param r Output value r of the ECDSA signature. + * @param s Output value s of the ECDSA signature. + * @dev v, r, s constitute `signer`'s signature on `msg.sender`. + */ + function authorizeAttestationSigner(address signer, uint8 v, bytes32 r, bytes32 s) public { + Account storage account = accounts[msg.sender]; + authorize(signer, v, r, s); + account.signers.attestation = signer; + emit AttestationSignerAuthorized(msg.sender, signer); + } + + /** + * @notice Returns the account associated with `signer`. + * @param signer The address of the account or currently authorized attestation signer. + * @dev Fails if the `signer` is not an account or currently authorized attestation signer. * @return The associated account. */ - function activeAttesttationSignerToAccount(address accountOrAttestationSigner) - external - view - returns (address) - { - address authorizingAccount = authorizedBy[accountOrAttestationSigner]; + function attestationSignerToAccount(address signer) external view returns (address) { + address authorizingAccount = authorizedBy[signer]; if (authorizingAccount != address(0)) { - require(accounts[authorizingAccount].signers.attesting == accountOrAttestationSigner); + require(accounts[authorizingAccount].signers.attestation == signer); return authorizingAccount; } else { - require(isAccount(accountOrAttestationSigner)); - return accountOrAttestationSigner; + require(isAccount(signer)); + return signer; } } /** - * @notice Returns the account associated with `accountOrVoteSigner`. - * @param accountOrVoteSigner The address of the account or active authorized vote signer. - * @dev Fails if the `accountOrVoteSigner` is not an account or active authorized vote signer. + * @notice Returns the account associated with `signer`. + * @param signer The address of an account or currently authorized validator signer. + * @dev Fails if the `signer` is not an account or currently authorized validator. * @return The associated account. */ - function activeVoteSignerToAccount(address accountOrVoteSigner) external view returns (address) { - address authorizingAccount = authorizedBy[accountOrVoteSigner]; + function validatorSignerToAccount(address signer) public view returns (address) { + address authorizingAccount = authorizedBy[signer]; if (authorizingAccount != address(0)) { - require(accounts[authorizingAccount].signers.voting == accountOrVoteSigner); + require(accounts[authorizingAccount].signers.validator == signer); return authorizingAccount; } else { - require(isAccount(accountOrVoteSigner)); - return accountOrVoteSigner; + require(isAccount(signer)); + return signer; } } /** - * @notice Returns the account associated with `accountOrVoteSigner`. - * @param accountOrVoteSigner The address of the account or previously authorized vote signer. - * @dev Fails if the `accountOrVoteSigner` is not an account or previously authorized vote signer. + * @notice Returns the account associated with `signer`. + * @param signer The address of the account or currently authorized vote signer. + * @dev Fails if the `signer` is not an account or currently authorized vote signer. * @return The associated account. */ - function voteSignerToAccount(address accountOrVoteSigner) external view returns (address) { - address authorizingAccount = authorizedBy[accountOrVoteSigner]; + function voteSignerToAccount(address signer) external view returns (address) { + address authorizingAccount = authorizedBy[signer]; if (authorizingAccount != address(0)) { + require(accounts[authorizingAccount].signers.vote == signer); return authorizingAccount; } else { - require(isAccount(accountOrVoteSigner)); - return accountOrVoteSigner; + require(isAccount(signer)); + return signer; } } + /** + * @notice Returns the account associated with `signer`. + * @param signer The address of the account or previously authorized signer. + * @dev Fails if the `signer` is not an account or previously authorized signer. + * @return The associated account. + */ + function signerToAccount(address signer) external view returns (address) { + address authorizingAccount = authorizedBy[signer]; + if (authorizingAccount != address(0)) { + return authorizingAccount; + } else { + require(isAccount(signer)); + return signer; + } + } + + /** + * @notice Returns the vote signer for the specified account. + * @param account The address of the account. + * @return The address with which the account can sign votes. + */ + function getVoteSigner(address account) public view returns (address) { + require(isAccount(account)); + address signer = accounts[account].signers.vote; + return signer == address(0) ? account : signer; + } + + /** + * @notice Returns the validator signer for the specified account. + * @param account The address of the account. + * @return The address with which the account can register a validator or group. + */ + function getValidatorSigner(address account) public view returns (address) { + require(isAccount(account)); + address signer = accounts[account].signers.validator; + return signer == address(0) ? account : signer; + } + + /** + * @notice Returns the attestation signer for the specified account. + * @param account The address of the account. + * @return The address with which the account can sign attestations. + */ + function getAttestationSigner(address account) public view returns (address) { + require(isAccount(account)); + address signer = accounts[account].signers.attestation; + return signer == address(0) ? account : signer; + } + /** * @notice Getter for the name of an account. * @param account The address of the account to get the name for. @@ -247,160 +372,6 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry { return accounts[account].walletAddress; } - /** - * @notice Creates an account. - * @return True if account creation succeeded. - */ - function createAccount() public returns (bool) { - require(isNotAccount(msg.sender) && isNotAuthorized(msg.sender)); - Account storage account = accounts[msg.sender]; - account.exists = true; - emit AccountCreated(msg.sender); - return true; - } - - /** - * @notice Setter for the name of an account. - * @param name The name to set. - */ - function setName(string memory name) public { - require(isAccount(msg.sender)); - accounts[msg.sender].name = name; - emit AccountNameSet(msg.sender, name); - } - - /** - * @notice Setter for the wallet address for an account - * @param walletAddress The wallet address to set for the account - */ - function setWalletAddress(address walletAddress) public { - require(isAccount(msg.sender)); - accounts[msg.sender].walletAddress = walletAddress; - emit AccountWalletAddressSet(msg.sender, walletAddress); - } - - /** - * @notice Setter for the data encryption key and version. - * @param dataEncryptionKey secp256k1 public key for data encryption. Preferably compressed. - */ - function setAccountDataEncryptionKey(bytes memory dataEncryptionKey) public { - require(dataEncryptionKey.length >= 33, "data encryption key length <= 32"); - accounts[msg.sender].dataEncryptionKey = dataEncryptionKey; - emit AccountDataEncryptionKeySet(msg.sender, dataEncryptionKey); - } - - /** - * @notice Authorizes an address to sign attestations on behalf of the account. - * @param attestor The address of the signing key to authorize. - * @param v The recovery id of the incoming ECDSA signature. - * @param r Output value r of the ECDSA signature. - * @param s Output value s of the ECDSA signature. - * @dev v, r, s constitute `attestor`'s signature on `msg.sender`. - */ - function authorizeAttestationSigner(address attestor, uint8 v, bytes32 r, bytes32 s) public { - Account storage account = accounts[msg.sender]; - authorize(attestor, account.signers.attesting, v, r, s); - account.signers.attesting = attestor; - emit AttestationSignerAuthorized(msg.sender, attestor); - } - - /** - * @notice Returns the account associated with `accountOrAttestationSigner`. - * @param accountOrAttestationSigner The address of the account or previously authorized - * attestation signing key. - * @dev Fails if the `accountOrAttestationSigner` is not an account or previously authorized - * attestation signing key. - * @return The associated account. - */ - function attestationSignerToAccount(address accountOrAttestationSigner) - public - view - returns (address) - { - address authorizingAccount = authorizedBy[accountOrAttestationSigner]; - if (authorizingAccount != address(0)) { - return authorizingAccount; - } else { - require(isAccount(accountOrAttestationSigner)); - return accountOrAttestationSigner; - } - } - - /** - * @notice Returns the account associated with `accountOrValidationSigner`. - * @param accountOrValidationSigner The address of the account or active authorized validator. - * @dev Fails if the `accountOrValidationSigner` is not an account or active authorized validator. - * @return The associated account. - */ - function activeValidationSignerToAccount(address accountOrValidationSigner) - public - view - returns (address) - { - address authorizingAccount = authorizedBy[accountOrValidationSigner]; - if (authorizingAccount != address(0)) { - require(accounts[authorizingAccount].signers.validating == accountOrValidationSigner); - return authorizingAccount; - } else { - require(isAccount(accountOrValidationSigner)); - return accountOrValidationSigner; - } - } - - /** - * @notice Returns the account associated with `accountOrValidationSigner`. - * @param accountOrValidationSigner The address of the account or previously authorized validator. - * @dev Fails if the `accountOrValidationSigner` is not an account or previously authorized - validator. - * @return The associated account. - */ - function validationSignerToAccount(address accountOrValidationSigner) - public - view - returns (address) - { - address authorizingAccount = authorizedBy[accountOrValidationSigner]; - if (authorizingAccount != address(0)) { - return authorizingAccount; - } else { - require(isAccount(accountOrValidationSigner)); - return accountOrValidationSigner; - } - } - - /** - * @notice Returns the vote signer for the specified account. - * @param account The address of the account. - * @return The address with which the account can sign votes. - */ - function getVoteSigner(address account) public view returns (address) { - require(isAccount(account)); - address voter = accounts[account].signers.voting; - return voter == address(0) ? account : voter; - } - - /** - * @notice Returns the validation signer for the specified account. - * @param account The address of the account. - * @return The address with which the account can register a validator or group. - */ - function getValidationSigner(address account) public view returns (address) { - require(isAccount(account)); - address validator = accounts[account].signers.validating; - return validator == address(0) ? account : validator; - } - - /** - * @notice Returns the attestation signer for the specified account. - * @param account The address of the account. - * @return The address with which the account can sign attestations. - */ - function getAttestationSigner(address account) public view returns (address) { - require(isAccount(account)); - address attestor = accounts[account].signers.attesting; - return attestor == address(0) ? account : attestor; - } - /** * @notice Check if an account already exists. * @param account The address of the account @@ -420,31 +391,39 @@ contract Accounts is IAccounts, ReentrancyGuard, Initializable, UsingRegistry { } /** - * @notice Check if an address has been authorized by an account for voting or validating. - * @param account The possibly authorized address. + * @notice Check if an address has been an authorized signer for an account. + * @param signer The possibly authorized address. + * @return Returns `true` if authorized. Returns `false` otherwise. + */ + function isAuthorizedSigner(address signer) external view returns (bool) { + return (authorizedBy[signer] != address(0)); + } + + /** + * @notice Check if an address has been an authorized signer for an account. + * @param signer The possibly authorized address. * @return Returns `false` if authorized. Returns `true` otherwise. */ - function isNotAuthorized(address account) internal view returns (bool) { - return (authorizedBy[account] == address(0)); + function isNotAuthorizedSigner(address signer) internal view returns (bool) { + return (authorizedBy[signer] == address(0)); } /** - * @notice Authorizes voting or validating power of `msg.sender`'s account to another address. - * @param current The address to authorize. - * @param previous The previous authorized address. + * @notice Authorizes some role of of `msg.sender`'s account to another address. + * @param authorized The address to authorize. * @param v The recovery id of the incoming ECDSA signature. * @param r Output value r of the ECDSA signature. * @param s Output value s of the ECDSA signature. * @dev Fails if the address is already authorized or is an account. + * @dev Note that once an address is authorized, it may never be authorized again. * @dev v, r, s constitute `current`'s signature on `msg.sender`. */ - function authorize(address current, address previous, uint8 v, bytes32 r, bytes32 s) private { - require(isAccount(msg.sender) && isNotAccount(current) && isNotAuthorized(current)); + function authorize(address authorized, uint8 v, bytes32 r, bytes32 s) private { + require(isAccount(msg.sender) && isNotAccount(authorized) && isNotAuthorizedSigner(authorized)); address signer = Signatures.getSignerOfAddress(msg.sender, v, r, s); - require(signer == current); + require(signer == authorized); - authorizedBy[previous] = address(0); - authorizedBy[current] = msg.sender; + authorizedBy[authorized] = msg.sender; } } diff --git a/packages/protocol/contracts/common/UsingPrecompiles.sol b/packages/protocol/contracts/common/UsingPrecompiles.sol index 4b93a055c67..542846af9a6 100644 --- a/packages/protocol/contracts/common/UsingPrecompiles.sol +++ b/packages/protocol/contracts/common/UsingPrecompiles.sol @@ -123,16 +123,20 @@ contract UsingPrecompiles { /** * @notice Checks a BLS proof of possession. - * @param proofOfPossessionBytes The public key and signature of the proof of possession. + * @param sender The address signed by the BLS key to generate the proof of possession. + * @param blsKey The BLS public key that the validator is using for consensus, should pass proof + * of possession. 48 bytes. + * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the + * account address. 96 bytes. * @return True upon success. */ - function checkProofOfPossession(address sender, bytes memory proofOfPossessionBytes) + function checkProofOfPossession(address sender, bytes memory blsKey, bytes memory blsPop) public returns (bool) { bool success; (success, ) = PROOF_OF_POSSESSION.call.value(0).gas(gasleft())( - abi.encodePacked(sender, proofOfPossessionBytes) + abi.encodePacked(sender, blsKey, blsPop) ); return success; } diff --git a/packages/protocol/contracts/common/interfaces/IAccounts.sol b/packages/protocol/contracts/common/interfaces/IAccounts.sol index b197c66b549..0360e66394e 100644 --- a/packages/protocol/contracts/common/interfaces/IAccounts.sol +++ b/packages/protocol/contracts/common/interfaces/IAccounts.sol @@ -2,13 +2,11 @@ pragma solidity ^0.5.3; interface IAccounts { function isAccount(address) external view returns (bool); - function activeVoteSignerToAccount(address) external view returns (address); function voteSignerToAccount(address) external view returns (address); - function activeValidationSignerToAccount(address) external view returns (address); - function validationSignerToAccount(address) external view returns (address); - function getValidationSigner(address) external view returns (address); - function activeAttesttationSignerToAccount(address) external view returns (address); + function validatorSignerToAccount(address) external view returns (address); function attestationSignerToAccount(address) external view returns (address); + function signerToAccount(address) external view returns (address); + function getValidatorSigner(address) external view returns (address); function getAttestationSigner(address) external view returns (address); function setAccountDataEncryptionKey(bytes calldata) external; diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 2ee97bc0e8d..c24bb9b6cbd 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -209,7 +209,7 @@ contract Election is require(votes.total.eligible.contains(group)); require(0 < value); require(canReceiveVotes(group, value)); - address account = getAccounts().activeVoteSignerToAccount(msg.sender); + address account = getAccounts().voteSignerToAccount(msg.sender); // Add group to the groups voted for by the account. address[] storage groups = votes.groupsVotedFor[account]; @@ -233,7 +233,7 @@ contract Election is * @dev Pending votes cannot be activated until an election has been held. */ function activate(address group) external nonReentrant returns (bool) { - address account = getAccounts().activeVoteSignerToAccount(msg.sender); + address account = getAccounts().voteSignerToAccount(msg.sender); PendingVote storage pendingVote = votes.pending.forGroup[group].byAccount[account]; require(pendingVote.epoch < getEpochNumber()); uint256 value = pendingVote.value; @@ -263,7 +263,7 @@ contract Election is uint256 index ) external nonReentrant returns (bool) { require(group != address(0)); - address account = getAccounts().activeVoteSignerToAccount(msg.sender); + address account = getAccounts().voteSignerToAccount(msg.sender); require(0 < value && value <= getPendingVotesForGroupByAccount(group, account)); decrementPendingVotes(group, account, value); decrementTotalVotes(group, value, lesser, greater); @@ -296,7 +296,7 @@ contract Election is ) external nonReentrant returns (bool) { // TODO(asa): Dedup with revokePending. require(group != address(0)); - address account = getAccounts().activeVoteSignerToAccount(msg.sender); + address account = getAccounts().voteSignerToAccount(msg.sender); require(0 < value && value <= getActiveVotesForGroupByAccount(group, account)); decrementActiveVotes(group, account, value); decrementTotalVotes(group, value, lesser, greater); @@ -706,7 +706,7 @@ contract Election is * @return The list of elected validators. * @dev See https://en.wikipedia.org/wiki/D%27Hondt_method#Allocation for more information. */ - function electValidators() external view returns (address[] memory) { + function electValidatorSigners() external view returns (address[] memory) { // Groups must have at least `electabilityThreshold` proportion of the total votes to be // considered for the election. uint256 requiredVotes = electabilityThreshold diff --git a/packages/protocol/contracts/governance/Governance.sol b/packages/protocol/contracts/governance/Governance.sol index 029aa8ee062..850a1dcb464 100644 --- a/packages/protocol/contracts/governance/Governance.sol +++ b/packages/protocol/contracts/governance/Governance.sol @@ -437,7 +437,7 @@ contract Governance is nonReentrant returns (bool) { - address account = getAccounts().activeVoteSignerToAccount(msg.sender); + address account = getAccounts().voteSignerToAccount(msg.sender); // TODO(asa): When upvoting a proposal that will get dequeued, should we let the tx succeed // and return false? dequeueProposalsIfReady(); @@ -484,7 +484,7 @@ contract Governance is */ function revokeUpvote(uint256 lesser, uint256 greater) external nonReentrant returns (bool) { dequeueProposalsIfReady(); - address account = getAccounts().activeVoteSignerToAccount(msg.sender); + address account = getAccounts().voteSignerToAccount(msg.sender); Voter storage voter = voters[account]; uint256 proposalId = voter.upvote.proposalId; Proposals.Proposal storage proposal = proposals[proposalId]; @@ -548,7 +548,7 @@ contract Governance is nonReentrant returns (bool) { - address account = getAccounts().activeVoteSignerToAccount(msg.sender); + address account = getAccounts().voteSignerToAccount(msg.sender); dequeueProposalsIfReady(); Proposals.Proposal storage proposal = proposals[proposalId]; require(isDequeuedProposal(proposal, proposalId, index)); diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index f5ef68f71b9..261fc914b8b 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -53,10 +53,6 @@ contract Validators is uint256 duration; } - // If we knew what time the validator was last in a group, we could enforce that to deregister a - // group, you need to have had 0 members for `duration`, and to deregister a validator, you need - // to have been out of a group for `duration`... - struct ValidatorGroup { bool exists; LinkedList.List members; @@ -85,8 +81,13 @@ contract Validators is uint256 lastRemovedFromGroupTimestamp; } + struct PublicKeys { + bytes ecdsa; + bytes bls; + } + struct Validator { - bytes publicKeysData; + PublicKeys publicKeys; address affiliation; FixidityLib.Fraction score; MembershipHistory membershipHistory; @@ -114,11 +115,12 @@ contract Validators is event GroupLockedGoldRequirementsSet(uint256 value, uint256 duration); event ValidatorLockedGoldRequirementsSet(uint256 value, uint256 duration); event MembershipHistoryLengthSet(uint256 length); - event ValidatorRegistered(address indexed validator, bytes publicKeysData); + event ValidatorRegistered(address indexed validator, bytes ecdsaPublicKey, bytes blsPublicKey); event ValidatorDeregistered(address indexed validator); event ValidatorAffiliated(address indexed validator, address indexed group); event ValidatorDeaffiliated(address indexed validator, address indexed group); - event ValidatorPublicKeysDataUpdated(address indexed validator, bytes publicKeysData); + event ValidatorEcdsaPublicKeyUpdated(address indexed validator, bytes ecdsaPublicKey); + event ValidatorBlsPublicKeyUpdated(address indexed validator, bytes blsPublicKey); event ValidatorGroupRegistered(address indexed group, uint256 commission); event ValidatorGroupDeregistered(address indexed group); event ValidatorGroupMemberAdded(address indexed group, address indexed validator); @@ -258,27 +260,32 @@ contract Validators is /** * @notice Registers a validator unaffiliated with any validator group. - * @param publicKeysData Comprised of three tightly-packed elements: - * - publicKey - The public key that the validator is using for consensus, should match - * msg.sender. 64 bytes. - * - blsPublicKey - The BLS public key that the validator is using for consensus, should pass - * proof of possession. 48 bytes. - * - blsPoP - The BLS public key proof of possession. 96 bytes. + * @param ecdsaPublicKey The ECDSA public key that the validator is using for consensus, should + * match the validator signer. 64 bytes. + * @param blsPublicKey The BLS public key that the validator is using for consensus, should pass + * proof of possession. 48 bytes. + * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the + * account address. 96 bytes. * @return True upon success. * @dev Fails if the account is already a validator or validator group. * @dev Fails if the account does not have sufficient Locked Gold. */ - function registerValidator(bytes calldata publicKeysData) external nonReentrant returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + function registerValidator( + bytes calldata ecdsaPublicKey, + bytes calldata blsPublicKey, + bytes calldata blsPop + ) external nonReentrant returns (bool) { + address account = getAccounts().signerToAccount(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); uint256 lockedGoldBalance = getLockedGold().getAccountTotalLockedGold(account); require(lockedGoldBalance >= validatorLockedGoldRequirements.value); Validator storage validator = validators[account]; - _updatePublicKeysData(validator, publicKeysData); - validator.publicKeysData = publicKeysData; + address signer = getAccounts().getValidatorSigner(account); + _updateEcdsaPublicKey(validator, signer, ecdsaPublicKey); + _updateBlsPublicKey(validator, account, blsPublicKey, blsPop); registeredValidators.push(account); updateMembershipHistory(account, address(0)); - emit ValidatorRegistered(account, publicKeysData); + emit ValidatorRegistered(account, ecdsaPublicKey, blsPublicKey); return true; } @@ -313,23 +320,23 @@ contract Validators is /** * @notice Updates a validator's score based on its uptime for the epoch. - * @param validator The address of the validator. + * @param signer The validator signer of the validator account whose score needs updating. * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. * @return True upon success. */ - function updateValidatorScore(address validator, uint256 uptime) external onlyVm() { - _updateValidatorScore(validator, uptime); + function updateValidatorScoreFromSigner(address signer, uint256 uptime) external onlyVm() { + _updateValidatorScoreFromSigner(signer, uptime); } /** * @notice Updates a validator's score based on its uptime for the epoch. - * @param validator The address of the validator. + * @param signer The validator signer of the validator whose score needs updating. * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. * @dev new_score = uptime ** exponent * adjustmentSpeed + old_score * (1 - adjustmentSpeed) * @return True upon success. */ - function _updateValidatorScore(address validator, uint256 uptime) internal { - address account = getAccounts().validationSignerToAccount(validator); + function _updateValidatorScoreFromSigner(address signer, uint256 uptime) internal { + address account = getAccounts().signerToAccount(signer); require(isValidator(account)); require(uptime <= FixidityLib.fixed1().unwrap()); @@ -361,32 +368,32 @@ contract Validators is } /** - * @notice Distributes epoch payments to `validator` and its group. - * @param validator The validator to distribute the epoch payment to. + * @notice Distributes epoch payments to the account associated with `signer` and its group. + * @param signer The validator signer of the account to distribute the epoch payment to. * @param maxPayment The maximum payment to the validator. Actual payment is based on score and * group commission. * @return The total payment paid to the validator and their group. */ - function distributeEpochPayment(address validator, uint256 maxPayment) + function distributeEpochPaymentsFromSigner(address signer, uint256 maxPayment) external onlyVm() returns (uint256) { - return _distributeEpochPayment(validator, maxPayment); + return _distributeEpochPaymentsFromSigner(signer, maxPayment); } /** - * @notice Distributes epoch payments to `validator` and its group. - * @param validator The validator to distribute the epoch payment to. + * @notice Distributes epoch payments to the account associated with `signer` and its group. + * @param signer The validator signer of the validator to distribute the epoch payment to. * @param maxPayment The maximum payment to the validator. Actual payment is based on score and * group commission. * @return The total payment paid to the validator and their group. */ - function _distributeEpochPayment(address validator, uint256 maxPayment) + function _distributeEpochPaymentsFromSigner(address signer, uint256 maxPayment) internal returns (uint256) { - address account = getAccounts().validationSignerToAccount(validator); + address account = getAccounts().signerToAccount(signer); require(isValidator(account)); // The group that should be paid is the group that the validator was a member of at the // time it was elected. @@ -414,7 +421,7 @@ contract Validators is * @dev Fails if the account is not a validator. */ function deregisterValidator(uint256 index) external nonReentrant returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); require(isValidator(account)); // Require that the validator has not been a member of a validator group for @@ -442,7 +449,7 @@ contract Validators is * @dev De-affiliates with the previously affiliated group if present. */ function affiliate(address group) external nonReentrant returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); require(isValidator(account) && isValidatorGroup(group)); require(meetsAccountLockedGoldRequirements(account)); require(meetsAccountLockedGoldRequirements(group)); @@ -461,7 +468,7 @@ contract Validators is * @dev Fails if the account is not a validator with non-zero affiliation. */ function deaffiliate() external nonReentrant returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); require(isValidator(account)); Validator storage validator = validators[account]; require(validator.affiliation != address(0)); @@ -470,46 +477,86 @@ contract Validators is } /** - * @notice Updates a validator's public keys data. - * @param publicKeysData Comprised of three tightly-packed elements: - * - publicKey - The public key that the validator is using for consensus, should match - * msg.sender. 64 bytes. - * - blsPublicKey - The BLS public key that the validator is using for consensus, should pass - * proof of possession. 48 bytes. - * - blsPoP - The BLS public key proof of possession. 96 bytes. + * @notice Updates a validator's BLS key. + * @param blsPublicKey The BLS public key that the validator is using for consensus, should pass + * proof of possession. 48 bytes. + * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the + * account address. 96 bytes. * @return True upon success. */ - function updatePublicKeysData(bytes calldata publicKeysData) external returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + function updateBlsPublicKey(bytes calldata blsPublicKey, bytes calldata blsPop) + external + returns (bool) + { + address account = getAccounts().signerToAccount(msg.sender); require(isValidator(account)); Validator storage validator = validators[account]; - _updatePublicKeysData(validator, publicKeysData); - emit ValidatorPublicKeysDataUpdated(account, publicKeysData); + _updateBlsPublicKey(validator, account, blsPublicKey, blsPop); + emit ValidatorBlsPublicKeyUpdated(account, blsPublicKey); return true; } /** - * @notice Updates a validator's public keys data. - * @param validator The validator whose public keys data should be updated. - * @param publicKeysData Comprised of three tightly-packed elements: - * - publicKey - The public key that the validator is using for consensus, should match - * msg.sender. 64 bytes. - * - blsPublicKey - The BLS public key that the validator is using for consensus, should pass - * proof of possession. 48 bytes. - * - blsPoP - The BLS public key proof of possession. 96 bytes. + * @notice Updates a validator's BLS key. + * @param validator The validator whose BLS public key should be updated. + * @param account The address under which the validator is registered. + * @param blsPublicKey The BLS public key that the validator is using for consensus, should pass + * proof of possession. 48 bytes. + * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the + * account address. 96 bytes. * @return True upon success. */ - function _updatePublicKeysData(Validator storage validator, bytes memory publicKeysData) - private + function _updateBlsPublicKey( + Validator storage validator, + address account, + bytes memory blsPublicKey, + bytes memory blsPop + ) private returns (bool) { + require(blsPublicKey.length == 48); + require(blsPop.length == 96); + require(checkProofOfPossession(account, blsPublicKey, blsPop)); + validator.publicKeys.bls = blsPublicKey; + return true; + } + + /** + * @notice Updates a validator's ECDSA key. + * @param account The address under which the validator is registered. + * @param signer The address which the validator is using to sign consensus messages. + * @param ecdsaPublicKey The ECDSA public key corresponding to `signer`. + * @return True upon success. + */ + function updateEcdsaPublicKey(address account, address signer, bytes calldata ecdsaPublicKey) + external + onlyRegisteredContract(ACCOUNTS_REGISTRY_ID) returns (bool) { + require(isValidator(account)); + Validator storage validator = validators[account]; + require(_updateEcdsaPublicKey(validator, signer, ecdsaPublicKey)); + emit ValidatorEcdsaPublicKeyUpdated(account, ecdsaPublicKey); + return true; + } + + /** + * @notice Updates a validator's ECDSA key. + * @param validator The validator whose ECDSA public key should be updated. + * @param signer The address with which the validator is signing consensus messages. + * @param ecdsaPublicKey The ECDSA public key that the validator is using for consensus. Should + * match `signer`. 64 bytes. + * @return True upon success. + */ + function _updateEcdsaPublicKey( + Validator storage validator, + address signer, + bytes memory ecdsaPublicKey + ) private returns (bool) { + require(ecdsaPublicKey.length == 64); require( - // secp256k1 public key + BLS public key + BLS proof of possession - publicKeysData.length == (64 + 48 + 96) + address(uint160(uint256(keccak256(ecdsaPublicKey)))) == signer, + "ECDSA key does not match signer" ); - // Use the proof of possession bytes - require(checkProofOfPossession(msg.sender, publicKeysData.slice(64, 48 + 96))); - validator.publicKeysData = publicKeysData; + validator.publicKeys.ecdsa = ecdsaPublicKey; return true; } @@ -523,7 +570,7 @@ contract Validators is */ function registerValidatorGroup(uint256 commission) external nonReentrant returns (bool) { require(commission <= FixidityLib.fixed1().unwrap(), "Commission can't be greater than 100%"); - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); uint256 lockedGoldBalance = getLockedGold().getAccountTotalLockedGold(account); require(lockedGoldBalance >= groupLockedGoldRequirements.value); @@ -542,7 +589,7 @@ contract Validators is * @dev Fails if the account is not a validator group with no members. */ function deregisterValidatorGroup(uint256 index) external nonReentrant returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); // Only Validator Groups that have never had members or have been empty for at least // `groupLockedGoldRequirements.duration` seconds can be deregistered. require(isValidatorGroup(account) && groups[account].members.numElements == 0); @@ -564,7 +611,7 @@ contract Validators is * @dev Fails if the group has zero members. */ function addMember(address validator) external nonReentrant returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); require(groups[account].members.numElements > 0); return _addMember(account, validator, address(0), address(0)); } @@ -583,7 +630,7 @@ contract Validators is nonReentrant returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); require(groups[account].members.numElements == 0); return _addMember(account, validator, lesser, greater); } @@ -626,7 +673,7 @@ contract Validators is * @dev Fails if `validator` is not a member of the account's group. */ function removeMember(address validator) external nonReentrant returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); require(isValidatorGroup(account) && isValidator(validator), "is not group and validator"); return _removeMember(account, validator); } @@ -646,7 +693,7 @@ contract Validators is nonReentrant returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); require(isValidatorGroup(account) && isValidator(validator)); ValidatorGroup storage group = groups[account]; require(group.members.contains(validator)); @@ -662,7 +709,7 @@ contract Validators is * @return True upon success. */ function updateCommission(uint256 commission) external returns (bool) { - address account = getAccounts().activeValidationSignerToAccount(msg.sender); + address account = getAccounts().signerToAccount(msg.sender); require(isValidatorGroup(account)); ValidatorGroup storage group = groups[account]; require(commission <= FixidityLib.fixed1().unwrap(), "Commission can't be greater than 100%"); @@ -706,19 +753,44 @@ contract Validators is return balance >= getAccountLockedGoldRequirement(account); } + /** + * @notice Returns the validator BLS key. + * @param signer The account that registered the validator or its authorized signing address. + * @return The validator BLS key. + */ + function getValidatorBlsPublicKeyFromSigner(address signer) + external + view + returns (bytes memory blsPublicKey) + { + address account = getAccounts().signerToAccount(signer); + require(isValidator(account)); + return validators[account].publicKeys.bls; + } + /** * @notice Returns validator information. * @param account The account that registered the validator. * @return The unpacked validator struct. */ function getValidator(address account) - external + public view - returns (bytes memory publicKeysData, address affiliation, uint256 score) + returns ( + bytes memory ecdsaPublicKey, + bytes memory blsPublicKey, + address affiliation, + uint256 score + ) { require(isValidator(account)); Validator storage validator = validators[account]; - return (validator.publicKeysData, validator.affiliation, validator.score.unwrap()); + return ( + validator.publicKeys.ecdsa, + validator.publicKeys.bls, + validator.affiliation, + validator.score.unwrap() + ); } /** @@ -760,7 +832,7 @@ contract Validators is address[] memory topAccounts = groups[account].members.headN(n); address[] memory topValidators = new address[](n); for (uint256 i = 0; i < n; i = i.add(1)) { - topValidators[i] = getAccounts().getValidationSigner(topAccounts[i]); + topValidators[i] = getAccounts().getValidatorSigner(topAccounts[i]); } return topValidators; } @@ -814,6 +886,19 @@ contract Validators is return registeredValidators; } + /** + * @notice Returns the list of signers for the registered validator accounts. + * @return The list of signers for registered validator accounts. + */ + function getRegisteredValidatorSigners() external view returns (address[] memory) { + IAccounts accounts = getAccounts(); + address[] memory signers = new address[](registeredValidators.length); + for (uint256 i = 0; i < signers.length; i = i.add(1)) { + signers[i] = accounts.getValidatorSigner(registeredValidators[i]); + } + return signers; + } + /** * @notice Returns the list of registered validator group accounts. * @return The list of registered validator group addresses. @@ -837,7 +922,7 @@ contract Validators is * @return Whether a particular address is a registered validator. */ function isValidator(address account) public view returns (bool) { - return validators[account].publicKeysData.length > 0; + return validators[account].publicKeys.bls.length > 0; } /** @@ -921,6 +1006,12 @@ contract Validators is return true; } + /** + * @notice Updates the size history of a validator group. + * @param group The account whose group size has changed. + * @param size The new size of the group. + * @dev Used to determine how much gold an account needs to keep locked. + */ function updateSizeHistory(address group, uint256 size) private { uint256[] storage sizeHistory = groups[group].sizeHistory; if (size == sizeHistory.length) { @@ -932,6 +1023,17 @@ contract Validators is } } + /** + * @notice Returns the group that `account` was a member of at the end of the last epoch. + * @param signer The signer of the account whose group membership should be returned. + * @return The group that `account` was a member of at the end of the last epoch. + */ + function getMembershipInLastEpochFromSigner(address signer) external view returns (address) { + address account = getAccounts().signerToAccount(signer); + require(isValidator(account)); + return getMembershipInLastEpoch(account); + } + /** * @notice Returns the group that `account` was a member of at the end of the last epoch. * @param account The account whose group membership should be returned. diff --git a/packages/protocol/contracts/governance/interfaces/IElection.sol b/packages/protocol/contracts/governance/interfaces/IElection.sol index 55c545b0d21..ea20aa76978 100644 --- a/packages/protocol/contracts/governance/interfaces/IElection.sol +++ b/packages/protocol/contracts/governance/interfaces/IElection.sol @@ -6,5 +6,5 @@ interface IElection { function getTotalVotesByAccount(address) external view returns (uint256); function markGroupIneligible(address) external; function markGroupEligible(address, address, address) external; - function electValidators() external view returns (address[] memory); + function electValidatorSigners() external view returns (address[] memory); } diff --git a/packages/protocol/contracts/governance/interfaces/IValidators.sol b/packages/protocol/contracts/governance/interfaces/IValidators.sol index 80906504a03..fcb6e3cd1ef 100644 --- a/packages/protocol/contracts/governance/interfaces/IValidators.sol +++ b/packages/protocol/contracts/governance/interfaces/IValidators.sol @@ -7,4 +7,6 @@ interface IValidators { function getGroupsNumMembers(address[] calldata) external view returns (uint256[] memory); function getNumRegisteredValidators() external view returns (uint256); function getTopGroupValidators(address, uint256) external view returns (address[] memory); + function updateEcdsaPublicKey(address, address, bytes calldata) external returns (bool); + function isValidator(address) external view returns (bool); } diff --git a/packages/protocol/contracts/governance/test/MockElection.sol b/packages/protocol/contracts/governance/test/MockElection.sol index ddc157abe79..f39b470ba0f 100644 --- a/packages/protocol/contracts/governance/test/MockElection.sol +++ b/packages/protocol/contracts/governance/test/MockElection.sol @@ -44,7 +44,7 @@ contract MockElection is IElection { electedValidators = _electedValidators; } - function electValidators() external view returns (address[] memory) { + function electValidatorSigners() external view returns (address[] memory) { return electedValidators; } } diff --git a/packages/protocol/contracts/governance/test/MockValidators.sol b/packages/protocol/contracts/governance/test/MockValidators.sol index 0eb1c902fb3..aa2406f8e0f 100644 --- a/packages/protocol/contracts/governance/test/MockValidators.sol +++ b/packages/protocol/contracts/governance/test/MockValidators.sol @@ -6,42 +6,33 @@ import "../interfaces/IValidators.sol"; * @title Holds a list of addresses of validators */ contract MockValidators is IValidators { - mapping(address => bool) private _isValidating; - mapping(address => bool) private _isVoting; + mapping(address => bool) public isValidator; mapping(address => uint256) private numGroupMembers; mapping(address => uint256) private lockedGoldRequirements; mapping(address => bool) private doesNotMeetAccountLockedGoldRequirements; mapping(address => address[]) private members; uint256 private numRegisteredValidators; - function setDoesNotMeetAccountLockedGoldRequirements(address account) external { - doesNotMeetAccountLockedGoldRequirements[account] = true; + function updateEcdsaPublicKey(address, address, bytes calldata) external returns (bool) { + return true; } - function meetsAccountLockedGoldRequirements(address account) external view returns (bool) { - return !doesNotMeetAccountLockedGoldRequirements[account]; + function setValidator(address account) external { + isValidator[account] = true; } - function isValidating(address account) external view returns (bool) { - return _isValidating[account]; + function setDoesNotMeetAccountLockedGoldRequirements(address account) external { + doesNotMeetAccountLockedGoldRequirements[account] = true; } - function isVoting(address account) external view returns (bool) { - return _isVoting[account]; + function meetsAccountLockedGoldRequirements(address account) external view returns (bool) { + return !doesNotMeetAccountLockedGoldRequirements[account]; } function getGroupNumMembers(address group) public view returns (uint256) { return members[group].length; } - function setValidating(address account) external { - _isValidating[account] = true; - } - - function setVoting(address account) external { - _isVoting[account] = true; - } - function setNumRegisteredValidators(uint256 value) external { numRegisteredValidators = value; } diff --git a/packages/protocol/contracts/governance/test/ValidatorsTest.sol b/packages/protocol/contracts/governance/test/ValidatorsTest.sol index a049ddc6efb..86af7f26a3b 100644 --- a/packages/protocol/contracts/governance/test/ValidatorsTest.sol +++ b/packages/protocol/contracts/governance/test/ValidatorsTest.sol @@ -7,14 +7,14 @@ import "../../common/FixidityLib.sol"; * @title A wrapper around Validators that exposes onlyVm functions for testing. */ contract ValidatorsTest is Validators { - function updateValidatorScore(address validator, uint256 uptime) external { - return _updateValidatorScore(validator, uptime); + function updateValidatorScoreFromSigner(address signer, uint256 uptime) external { + return _updateValidatorScoreFromSigner(signer, uptime); } - function distributeEpochPayment(address validator, uint256 maxPayment) + function distributeEpochPaymentsFromSigner(address signer, uint256 maxPayment) external returns (uint256) { - return _distributeEpochPayment(validator, maxPayment); + return _distributeEpochPaymentsFromSigner(signer, maxPayment); } } diff --git a/packages/protocol/contracts/identity/Attestations.sol b/packages/protocol/contracts/identity/Attestations.sol index c3758e906b0..fc4287a77e6 100644 --- a/packages/protocol/contracts/identity/Attestations.sol +++ b/packages/protocol/contracts/identity/Attestations.sol @@ -501,7 +501,7 @@ contract Attestations is ) public view returns (address) { bytes32 codehash = keccak256(abi.encodePacked(identifier, account)); address signer = Signatures.getSignerOfMessageHash(codehash, v, r, s); - address issuer = getAccounts().activeAttesttationSignerToAccount(signer); + address issuer = getAccounts().attestationSignerToAccount(signer); Attestation storage attestation = identifiers[identifier].attestations[account] .issuedAttestations[issuer]; @@ -570,7 +570,7 @@ contract Attestations is while (currentIndex < unselectedRequest.attestationsRequested) { seed = keccak256(abi.encodePacked(seed)); validator = validatorAddressFromCurrentSet(uint256(seed) % numberValidators); - issuer = getAccounts().activeValidationSignerToAccount(validator); + issuer = getAccounts().validatorSignerToAccount(validator); Attestation storage attestation = state.issuedAttestations[issuer]; // Attestation issuers can only be added if they haven't been already. diff --git a/packages/protocol/lib/test-utils.ts b/packages/protocol/lib/test-utils.ts index 812669b0f5a..0605852eaed 100644 --- a/packages/protocol/lib/test-utils.ts +++ b/packages/protocol/lib/test-utils.ts @@ -34,30 +34,6 @@ export function assertContainSubset(superset: any, subset: any) { return assert2.containSubset(superset, subset) } -export async function advanceBlockNum(numBlocks: number, web3: Web3) { - let returnValue: any - for (let i: number = 0; i < numBlocks; i++) { - returnValue = new Promise((resolve, reject) => { - web3.currentProvider.send( - { - jsonrpc: '2.0', - method: 'evm_mine', - params: [], - id: new Date().getTime(), - }, - // @ts-ignore - (err: any, result: any) => { - if (err) { - return reject(err) - } - return resolve(result) - } - ) - }) - } - return returnValue -} - export async function jsonRpc(web3: Web3, method: string, params: any[] = []): Promise { return new Promise((resolve, reject) => { web3.currentProvider.send( @@ -342,7 +318,6 @@ export const matchAny = () => { } export default { - advanceBlockNum, assertContainSubset, assertRevert, timeTravel, diff --git a/packages/protocol/lib/web3-utils.ts b/packages/protocol/lib/web3-utils.ts index d5b96f21e8e..b7dc2624564 100644 --- a/packages/protocol/lib/web3-utils.ts +++ b/packages/protocol/lib/web3-utils.ts @@ -3,41 +3,20 @@ import { setAndInitializeImplementation } from '@celo/protocol/lib/proxy-utils' import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { signTransaction } from '@celo/protocol/lib/signing-utils' +import { privateKeyToAddress } from '@celo/utils/lib/address' import { BigNumber } from 'bignumber.js' -import { ec as EC } from 'elliptic' import { EscrowInstance, GoldTokenInstance, MultiSigInstance, OwnableInstance, ProxyContract, ProxyInstance, RegistryInstance, StableTokenInstance } from 'types' import { TransactionObject } from 'web3/eth/types' import Web3 = require('web3') -const ec = new EC('secp256k1') -const cachedWeb3 = new Web3() - -export function add0x(str: string) { - return '0x' + str -} - -export function generatePublicKeyFromPrivateKey(privateKey: string) { - const ecPrivateKey = ec.keyFromPrivate(Buffer.from(privateKey, 'hex')) - const ecPublicKey: string = ecPrivateKey.getPublic('hex') - return ecPublicKey.slice(2) -} - -export function generateAccountAddressFromPrivateKey(privateKey: string) { - if (!privateKey.startsWith('0x')) { - privateKey = '0x' + privateKey - } - // @ts-ignore-next-line - return cachedWeb3.eth.accounts.privateKeyToAccount(privateKey).address -} - export async function sendTransactionWithPrivateKey( web3: Web3, tx: TransactionObject, privateKey: string, txArgs: any ) { - const address = generateAccountAddressFromPrivateKey(privateKey.slice(2)) + const address = privateKeyToAddress(privateKey) const encodedTxData = tx.encodeABI() const estimatedGas = await tx.estimateGas({ ...txArgs, diff --git a/packages/protocol/migrations/10_accounts.ts b/packages/protocol/migrations/10_accounts.ts index 4a9542cdc76..592087734c8 100644 --- a/packages/protocol/migrations/10_accounts.ts +++ b/packages/protocol/migrations/10_accounts.ts @@ -1,9 +1,21 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' -import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' -import { AccountsInstance } from 'types' +import { + deploymentForCoreContract, + getDeployedProxiedContract, +} from '@celo/protocol/lib/web3-utils' +import { AccountsInstance, RegistryInstance } from 'types' + +const initializeArgs = async (): Promise<[string]> => { + const registry: RegistryInstance = await getDeployedProxiedContract( + 'Registry', + artifacts + ) + return [registry.address] +} module.exports = deploymentForCoreContract( web3, artifacts, - CeloContractName.Accounts + CeloContractName.Accounts, + initializeArgs ) diff --git a/packages/protocol/migrations/20_elect_validators.ts b/packages/protocol/migrations/20_elect_validators.ts index a42077e0ba2..24baec893e4 100644 --- a/packages/protocol/migrations/20_elect_validators.ts +++ b/packages/protocol/migrations/20_elect_validators.ts @@ -1,17 +1,14 @@ /* tslint:disable:no-console */ import { NULL_ADDRESS } from '@celo/protocol/lib/test-utils' import { - add0x, - generateAccountAddressFromPrivateKey, - generatePublicKeyFromPrivateKey, getDeployedProxiedContract, sendTransactionWithPrivateKey, } from '@celo/protocol/lib/web3-utils' import { config } from '@celo/protocol/migrationsConfig' -import { blsPrivateKeyToProcessedPrivateKey } from '@celo/utils/lib/bls' +import { privateKeyToAddress, privateKeyToPublicKey } from '@celo/utils/lib/address' +import { getBlsPoP, getBlsPublicKey } from '@celo/utils/lib/bls' import { toFixed } from '@celo/utils/lib/fixidity' import { BigNumber } from 'bignumber.js' -import * as bls12377js from 'bls12377js' import { AccountsInstance, ElectionInstance, LockedGoldInstance, ValidatorsInstance } from 'types' const Web3 = require('web3') @@ -66,7 +63,7 @@ async function registerValidatorGroup( ) await web3.eth.sendTransaction({ - from: generateAccountAddressFromPrivateKey(privateKey.slice(0)), + from: privateKeyToAddress(privateKey), to: account.address, value: lockedGoldValue.times(1.01).toFixed(), // Add a premium to cover tx fees }) @@ -102,21 +99,6 @@ async function registerValidator( index: number, networkName: string ) { - const validatorPrivateKeyHexStripped = validatorPrivateKey.slice(2) - const address = generateAccountAddressFromPrivateKey(validatorPrivateKey) - const publicKey = generatePublicKeyFromPrivateKey(validatorPrivateKeyHexStripped) - const blsValidatorPrivateKeyBytes = blsPrivateKeyToProcessedPrivateKey( - validatorPrivateKeyHexStripped - ) - const blsPublicKey = bls12377js.BLS.privateToPublicBytes(blsValidatorPrivateKeyBytes).toString( - 'hex' - ) - const blsPoP = bls12377js.BLS.signPoP( - blsValidatorPrivateKeyBytes, - Buffer.from(address.slice(2), 'hex') - ).toString('hex') - const publicKeysData = publicKey + blsPublicKey + blsPoP - await lockGold( accounts, lockedGold, @@ -130,8 +112,12 @@ async function registerValidator( to: accounts.address, }) + const publicKey = privateKeyToPublicKey(validatorPrivateKey) + const blsPublicKey = getBlsPublicKey(validatorPrivateKey) + const blsPoP = getBlsPoP(privateKeyToAddress(validatorPrivateKey), validatorPrivateKey) + // @ts-ignore - const registerTx = validators.contract.methods.registerValidator(add0x(publicKeysData)) + const registerTx = validators.contract.methods.registerValidator(publicKey, blsPublicKey, blsPoP) await sendTransactionWithPrivateKey(web3, registerTx, validatorPrivateKey, { to: validators.address, @@ -146,7 +132,7 @@ async function registerValidator( // @ts-ignore const registerDataEncryptionKeyTx = accounts.contract.methods.setAccountDataEncryptionKey( - add0x(publicKey) + privateKeyToPublicKey(validatorPrivateKey) ) await sendTransactionWithPrivateKey(web3, registerDataEncryptionKeyTx, validatorPrivateKey, { @@ -216,7 +202,7 @@ module.exports = async (_deployer: any, networkName: string) => { console.info(' Adding Validators to Validator Group ...') for (let i = 0; i < valKeys.length; i++) { const key = valKeys[i] - const address = generateAccountAddressFromPrivateKey(key.slice(2)) + const address = privateKeyToAddress(key) if (i === 0) { // @ts-ignore const addTx = validators.contract.methods.addFirstMember(address, NULL_ADDRESS, NULL_ADDRESS) diff --git a/packages/protocol/package.json b/packages/protocol/package.json index fb42227a875..1e3b6884ec9 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -43,7 +43,6 @@ "@0x/subproviders": "^5.0.0", "@celo/utils": "^0.1.0", "apollo-client": "^2.4.13", - "bls12377js": "https://github.com/celo-org/bls12377js#cada1105f4a5e4c2ddd239c1874df3bf33144a10", "chai-subset": "^1.6.0", "csv-parser": "^2.0.0", "csv-stringify": "^4.3.1", diff --git a/packages/protocol/test/common/accounts.ts b/packages/protocol/test/common/accounts.ts index 6e409ed4543..86c65839056 100644 --- a/packages/protocol/test/common/accounts.ts +++ b/packages/protocol/test/common/accounts.ts @@ -1,14 +1,23 @@ import { parseSolidityStringArray } from '@celo/utils/lib/parsing' import { upperFirst } from 'lodash' -import { AccountsInstance } from 'types' -import { getParsedSignatureOfAddress } from '../../lib/signing-utils' +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { getParsedSignatureOfAddress } from '@celo/protocol/lib/signing-utils' import { assertLogMatches, assertLogMatches2, assertRevert, NULL_ADDRESS, -} from '../../lib/test-utils' -const Accounts: Truffle.Contract = artifacts.require('Accounts') +} from '@celo/protocol/lib/test-utils' +import { + AccountsContract, + AccountsInstance, + MockValidatorsContract, + MockValidatorsInstance, + RegistryContract, +} from 'types' +const Accounts: AccountsContract = artifacts.require('Accounts') +const Registry: RegistryContract = artifacts.require('Registry') +const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') const authorizationTests: any = {} const authorizationTestDescriptions = { voting: { @@ -16,8 +25,8 @@ const authorizationTestDescriptions = { subject: 'voteSigner', }, validating: { - me: 'validation signing key', - subject: 'validationSigner', + me: 'validator signing key', + subject: 'validatorSigner', }, attesting: { me: 'attestation signing key', @@ -27,6 +36,7 @@ const authorizationTestDescriptions = { contract('Accounts', (accounts: string[]) => { let accountsInstance: AccountsInstance + let mockValidators: MockValidatorsInstance const account = accounts[0] const caller = accounts[0] @@ -39,27 +49,28 @@ contract('Accounts', (accounts: string[]) => { beforeEach(async () => { accountsInstance = await Accounts.new({ from: account }) + mockValidators = await MockValidators.new() + const registry = await Registry.new() + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + await accountsInstance.initialize(registry.address) authorizationTests.voting = { fn: accountsInstance.authorizeVoteSigner, eventName: 'VoteSignerAuthorized', getAuthorizedFromAccount: accountsInstance.getVoteSigner, - getAccountFromAuthorized: accountsInstance.voteSignerToAccount, - getAccountFromActiveAuthorized: accountsInstance.activeVoteSignerToAccount, + authorizedSignerToAccount: accountsInstance.voteSignerToAccount, } authorizationTests.validating = { - fn: accountsInstance.authorizeValidationSigner, - eventName: 'ValidationSignerAuthorized', - getAuthorizedFromAccount: accountsInstance.getValidationSigner, - getAccountFromAuthorized: accountsInstance.validationSignerToAccount, - getAccountFromActiveAuthorized: accountsInstance.activeValidationSignerToAccount, + fn: accountsInstance.authorizeValidatorSigner, + eventName: 'ValidatorSignerAuthorized', + getAuthorizedFromAccount: accountsInstance.getValidatorSigner, + authorizedSignerToAccount: accountsInstance.validatorSignerToAccount, } authorizationTests.attesting = { fn: accountsInstance.authorizeAttestationSigner, eventName: 'AttestationSignerAuthorized', getAuthorizedFromAccount: accountsInstance.getAttestationSigner, - getAccountFromAuthorized: accountsInstance.attestationSignerToAccount, - getAccountFromActiveAuthorized: accountsInstance.activeAttesttationSignerToAccount, + authorizedSignerToAccount: accountsInstance.attestationSignerToAccount, } }) @@ -359,7 +370,7 @@ contract('Accounts', (accounts: string[]) => { await authorizationTest.fn(authorized, sig.v, sig.r, sig.s) assert.equal(await accountsInstance.authorizedBy(authorized), account) assert.equal(await authorizationTest.getAuthorizedFromAccount(account), authorized) - assert.equal(await authorizationTest.getAccountFromActiveAuthorized(authorized), account) + assert.equal(await authorizationTest.authorizedSignerToAccount(authorized), account) }) it(`should emit the right event`, async () => { @@ -409,14 +420,11 @@ contract('Accounts', (accounts: string[]) => { it(`should set the new authorized ${authorizationTestDescriptions[key].me}`, async () => { assert.equal(await accountsInstance.authorizedBy(newAuthorized), account) assert.equal(await authorizationTest.getAuthorizedFromAccount(account), newAuthorized) - assert.equal( - await authorizationTest.getAccountFromActiveAuthorized(newAuthorized), - account - ) + assert.equal(await authorizationTest.authorizedSignerToAccount(newAuthorized), account) }) - it('should reset the previous authorization', async () => { - assert.equal(await accountsInstance.authorizedBy(authorized), NULL_ADDRESS) + it('should preserve the previous authorization', async () => { + assert.equal(await accountsInstance.authorizedBy(authorized), account) }) }) }) @@ -426,11 +434,11 @@ contract('Accounts', (accounts: string[]) => { authorizationTestDescriptions[key].me }`, () => { it('should return the account when passed the account', async () => { - assert.equal(await authorizationTest.getAccountFromActiveAuthorized(account), account) + assert.equal(await authorizationTest.authorizedSignerToAccount(account), account) }) it('should revert when passed an address that is not an account', async () => { - await assertRevert(authorizationTest.getAccountFromActiveAuthorized(accounts[1])) + await assertRevert(authorizationTest.authorizedSignerToAccount(accounts[1])) }) }) @@ -444,16 +452,13 @@ contract('Accounts', (accounts: string[]) => { }) it('should return the account when passed the account', async () => { - assert.equal(await authorizationTest.getAccountFromActiveAuthorized(account), account) + assert.equal(await authorizationTest.authorizedSignerToAccount(account), account) }) it(`should return the account when passed the ${ authorizationTestDescriptions[key].me }`, async () => { - assert.equal( - await authorizationTest.getAccountFromActiveAuthorized(authorized), - account - ) + assert.equal(await authorizationTest.authorizedSignerToAccount(authorized), account) }) }) }) diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts index b4469376c56..c0b98704a9b 100644 --- a/packages/protocol/test/governance/election.ts +++ b/packages/protocol/test/governance/election.ts @@ -751,7 +751,7 @@ contract('Election', (accounts: string[]) => { }) }) - describe('#electValidators', () => { + describe('#electValidatorSigners', () => { let random: MockRandomInstance let totalLockedGold: number const group1 = accounts[0] @@ -812,7 +812,7 @@ contract('Election', (accounts: string[]) => { it("should return that group's member list", async () => { await setRandomness(hash1) - assertSameAddresses(await election.electValidators(), [ + assertSameAddresses(await election.electValidatorSigners(), [ validator1, validator2, validator3, @@ -830,7 +830,7 @@ contract('Election', (accounts: string[]) => { it('should return maxElectableValidators elected validators', async () => { await setRandomness(hash1) - assertSameAddresses(await election.electValidators(), [ + assertSameAddresses(await election.electValidatorSigners(), [ validator1, validator2, validator3, @@ -850,9 +850,9 @@ contract('Election', (accounts: string[]) => { it('should return different results', async () => { await setRandomness(hash1) - const valsWithHash1 = (await election.electValidators()).map((x) => x.toLowerCase()) + const valsWithHash1 = (await election.electValidatorSigners()).map((x) => x.toLowerCase()) await setRandomness(hash2) - const valsWithHash2 = (await election.electValidators()).map((x) => x.toLowerCase()) + const valsWithHash2 = (await election.electValidatorSigners()).map((x) => x.toLowerCase()) assert.sameMembers(valsWithHash1, valsWithHash2) assert.notDeepEqual(valsWithHash1, valsWithHash2) }) @@ -872,7 +872,7 @@ contract('Election', (accounts: string[]) => { it('should elect only n members from that group', async () => { await setRandomness(hash1) - assertSameAddresses(await election.electValidators(), [ + assertSameAddresses(await election.electValidatorSigners(), [ validator7, validator1, validator2, @@ -894,7 +894,7 @@ contract('Election', (accounts: string[]) => { it('should not elect any members from that group', async () => { await setRandomness(hash1) - assertSameAddresses(await election.electValidators(), [ + assertSameAddresses(await election.electValidatorSigners(), [ validator1, validator2, validator3, @@ -913,7 +913,7 @@ contract('Election', (accounts: string[]) => { it('should revert', async () => { await setRandomness(hash1) - await assertRevert(election.electValidators()) + await assertRevert(election.electValidatorSigners()) }) }) }) diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index 749a8581df3..387b5fdbd62 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -1,4 +1,5 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { getParsedSignatureOfAddress } from '@celo/protocol/lib/signing-utils' import { assertContainSubset, assertEqualBN, @@ -10,6 +11,7 @@ import { timeTravel, } from '@celo/protocol/lib/test-utils' import { fixed1, fromFixed, toFixed } from '@celo/utils/lib/fixidity' +import { addressToPublicKey } from '@celo/utils/lib/signatureUtils' import BigNumber from 'bignumber.js' import { AccountsContract, @@ -25,8 +27,8 @@ import { ValidatorsTestContract, ValidatorsTestInstance, } from 'types' -const Accounts: AccountsContract = artifacts.require('Accounts') +const Accounts: AccountsContract = artifacts.require('Accounts') const Validators: ValidatorsTestContract = artifacts.require('ValidatorsTest') const MockElection: MockElectionContract = artifacts.require('MockElection') const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') @@ -39,9 +41,10 @@ Validators.numberFormat = 'BigNumber' const parseValidatorParams = (validatorParams: any) => { return { - publicKeysData: validatorParams[0], - affiliation: validatorParams[1], - score: validatorParams[2], + ecdsaPublicKey: validatorParams[0], + blsPublicKey: validatorParams[1], + affiliation: validatorParams[2], + score: validatorParams[3], } } @@ -90,24 +93,26 @@ contract('Validators', (accounts: string[]) => { const maxGroupSize = new BigNumber(5) // A random 64 byte hex string. - const publicKey = - 'ea0733ad275e2b9e05541341a97ee82678c58932464fad26164657a111a7e37a9fa0300266fb90e2135a1f1512350cb4e985488a88809b14e3cbe415e76e82b2' const blsPublicKey = - '4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' + '0x4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' const blsPoP = - '9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' - const publicKeysData = '0x' + publicKey + blsPublicKey + blsPoP + '0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' const commission = toFixed(1 / 100) beforeEach(async () => { accountsInstance = await Accounts.new() - await Promise.all(accounts.map((account) => accountsInstance.createAccount({ from: account }))) + // Do not register an account for the last address so it can be used as an authorized validator signer. + await Promise.all( + accounts.slice(0, -1).map((account) => accountsInstance.createAccount({ from: account })) + ) mockElection = await MockElection.new() mockLockedGold = await MockLockedGold.new() registry = await Registry.new() validators = await Validators.new() + await accountsInstance.initialize(registry.address) await registry.setAddressFor(CeloContractName.Accounts, accountsInstance.address) await registry.setAddressFor(CeloContractName.Election, mockElection.address) await registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) + await registry.setAddressFor(CeloContractName.Validators, validators.address) await validators.initialize( registry.address, groupLockedGoldRequirements.value, @@ -123,9 +128,14 @@ contract('Validators', (accounts: string[]) => { const registerValidator = async (validator: string) => { await mockLockedGold.setAccountTotalLockedGold(validator, validatorLockedGoldRequirements.value) + const publicKey = await addressToPublicKey(validator, web3.eth.sign) await validators.registerValidator( // @ts-ignore bytes type - publicKeysData, + publicKey, + // @ts-ignore bytes type + blsPublicKey, + // @ts-ignore bytes type + blsPoP, { from: validator } ) } @@ -519,78 +529,110 @@ contract('Validators', (accounts: string[]) => { const validator = accounts[0] let resp: any describe('when the account is not a registered validator', () => { - let validatorRegistrationEpochNumber: number beforeEach(async () => { await mockLockedGold.setAccountTotalLockedGold( validator, validatorLockedGoldRequirements.value ) - resp = await validators.registerValidator( - // @ts-ignore bytes type - publicKeysData - ) - const blockNumber = (await web3.eth.getBlock('latest')).number - validatorRegistrationEpochNumber = Math.floor(blockNumber / EPOCH) }) - it('should mark the account as a validator', async () => { - assert.isTrue(await validators.isValidator(validator)) - }) + describe('when the account has authorized a validator signer', () => { + let validatorRegistrationEpochNumber: number + let publicKey: string + beforeEach(async () => { + const signer = accounts[9] + const sig = await getParsedSignatureOfAddress(web3, validator, signer) + await accountsInstance.authorizeValidatorSigner(signer, sig.v, sig.r, sig.s) + publicKey = await addressToPublicKey(signer, web3.eth.sign) + resp = await validators.registerValidator( + // @ts-ignore bytes type + publicKey, + // @ts-ignore bytes type + blsPublicKey, + // @ts-ignore bytes type + blsPoP + ) + const blockNumber = await web3.eth.getBlockNumber() + validatorRegistrationEpochNumber = Math.floor(blockNumber / EPOCH) + }) - it('should add the account to the list of validators', async () => { - assert.deepEqual(await validators.getRegisteredValidators(), [validator]) - }) + it('should mark the account as a validator', async () => { + assert.isTrue(await validators.isValidator(validator)) + }) - it('should set the validator public key', async () => { - const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.publicKeysData, publicKeysData) - }) + it('should add the account to the list of validators', async () => { + assert.deepEqual(await validators.getRegisteredValidators(), [validator]) + }) - it('should set account locked gold requirements', async () => { - const requirement = await validators.getAccountLockedGoldRequirement(validator) - assertEqualBN(requirement, validatorLockedGoldRequirements.value) - }) + it('should set the validator ecdsa public key', async () => { + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assert.equal(parsedValidator.ecdsaPublicKey, publicKey) + }) - it('should set the validator membership history', async () => { - const membershipHistory = await validators.getMembershipHistory(validator) - assertEqualBNArray(membershipHistory[0], [validatorRegistrationEpochNumber]) - assert.deepEqual(membershipHistory[1], [NULL_ADDRESS]) - }) + it('should set the validator bls public key', async () => { + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assert.equal(parsedValidator.blsPublicKey, blsPublicKey) + }) - it('should set the validator membership history', async () => { - const membershipHistory = await validators.getMembershipHistory(validator) - assertEqualBNArray(membershipHistory[0], [validatorRegistrationEpochNumber]) - assert.deepEqual(membershipHistory[1], [NULL_ADDRESS]) - }) + it('should set account locked gold requirements', async () => { + const requirement = await validators.getAccountLockedGoldRequirement(validator) + assertEqualBN(requirement, validatorLockedGoldRequirements.value) + }) - it('should emit the ValidatorRegistered event', async () => { - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorRegistered', - args: { - validator, - publicKeysData, - }, + it('should set the validator membership history', async () => { + const membershipHistory = await validators.getMembershipHistory(validator) + assertEqualBNArray(membershipHistory[0], [validatorRegistrationEpochNumber]) + assert.deepEqual(membershipHistory[1], [NULL_ADDRESS]) + }) + + it('should set the validator membership history', async () => { + const membershipHistory = await validators.getMembershipHistory(validator) + assertEqualBNArray(membershipHistory[0], [validatorRegistrationEpochNumber]) + assert.deepEqual(membershipHistory[1], [NULL_ADDRESS]) + }) + + it('should emit the ValidatorRegistered event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorRegistered', + args: { + validator, + ecdsaPublicKey: publicKey, + blsPublicKey: blsPublicKey, + }, + }) }) }) }) describe('when the account is already a registered validator ', () => { + let publicKey: string beforeEach(async () => { await mockLockedGold.setAccountTotalLockedGold( validator, validatorLockedGoldRequirements.value ) - // @ts-ignore bytes type - await validators.registerValidator(publicKeysData) }) it('should revert', async () => { + publicKey = await addressToPublicKey(validator, web3.eth.sign) + await validators.registerValidator( + // @ts-ignore bytes type + publicKey, + // @ts-ignore bytes type + blsPublicKey, + // @ts-ignore bytes type + blsPoP + ) await assertRevert( validators.registerValidator( // @ts-ignore bytes type - publicKeysData + publicKey, + // @ts-ignore bytes type + blsPublicKey, + // @ts-ignore bytes type + blsPoP ) ) }) @@ -603,10 +645,15 @@ contract('Validators', (accounts: string[]) => { }) it('should revert', async () => { + const publicKey = await addressToPublicKey(validator, web3.eth.sign) await assertRevert( validators.registerValidator( // @ts-ignore bytes type - publicKeysData + publicKey, + // @ts-ignore bytes type + blsPublicKey, + // @ts-ignore bytes type + blsPoP ) ) }) @@ -621,10 +668,15 @@ contract('Validators', (accounts: string[]) => { }) it('should revert', async () => { + const publicKey = await addressToPublicKey(validator, web3.eth.sign) await assertRevert( validators.registerValidator( // @ts-ignore bytes type - publicKeysData + publicKey, + // @ts-ignore bytes type + blsPublicKey, + // @ts-ignore bytes type + blsPoP ) ) }) @@ -750,7 +802,7 @@ contract('Validators', (accounts: string[]) => { describe('when the account has a registered validator', () => { beforeEach(async () => { await registerValidator(validator) - registrationEpoch = Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + registrationEpoch = Math.floor((await web3.eth.getBlockNumber()) / EPOCH) }) describe('when affiliating with a registered validator group', () => { beforeEach(async () => { @@ -831,9 +883,9 @@ contract('Validators', (accounts: string[]) => { await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: group, }) - additionEpoch = Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + additionEpoch = Math.floor((await web3.eth.getBlockNumber()) / EPOCH) resp = await validators.affiliate(otherGroup) - affiliationEpoch = Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + affiliationEpoch = Math.floor((await web3.eth.getBlockNumber()) / EPOCH) }) it('should remove the validator from the group membership list', async () => { @@ -931,7 +983,7 @@ contract('Validators', (accounts: string[]) => { let registrationEpoch: number beforeEach(async () => { await registerValidator(validator) - registrationEpoch = Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + registrationEpoch = Math.floor((await web3.eth.getBlockNumber()) / EPOCH) await registerValidatorGroup(group) await validators.affiliate(group) }) @@ -961,9 +1013,9 @@ contract('Validators', (accounts: string[]) => { let resp: any beforeEach(async () => { await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: group }) - additionEpoch = Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + additionEpoch = Math.floor((await web3.eth.getBlockNumber()) / EPOCH) resp = await validators.deaffiliate() - deaffiliationEpoch = Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + deaffiliationEpoch = Math.floor((await web3.eth.getBlockNumber()) / EPOCH) }) it('should remove the validator from the group membership list', async () => { @@ -1016,53 +1068,116 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('#updatePublicKeysData()', () => { - const newPublicKey = web3.utils.randomHex(64).slice(2) - const newBlsPublicKey = web3.utils.randomHex(48).slice(2) - const newBlsPoP = web3.utils.randomHex(96).slice(2) - const newPublicKeysData = '0x' + newPublicKey + newBlsPublicKey + newBlsPoP + describe('#updateEcdsaPublicKey()', () => { + describe('when called by a registered validator', () => { + const validator = accounts[0] + beforeEach(async () => { + await registerValidator(validator) + }) + + describe('when called by the registered `Accounts` contract', () => { + beforeEach(async () => { + await registry.setAddressFor(CeloContractName.Accounts, accounts[0]) + }) + + describe('when the public key matches the signer', () => { + let resp: any + let newPublicKey: string + const signer = accounts[9] + beforeEach(async () => { + newPublicKey = await addressToPublicKey(signer, web3.eth.sign) + // @ts-ignore Broken typechain typing for bytes + resp = await validators.updateEcdsaPublicKey(validator, signer, newPublicKey) + }) + + it('should set the validator ecdsa public key', async () => { + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assert.equal(parsedValidator.ecdsaPublicKey, newPublicKey) + }) + + it('should emit the ValidatorEcdsaPublicKeyUpdated event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorEcdsaPublicKeyUpdated', + args: { + validator, + ecdsaPublicKey: newPublicKey, + }, + }) + }) + }) + + describe('when the public key does not match the signer', () => { + let newPublicKey: string + const signer = accounts[9] + it('should revert', async () => { + newPublicKey = await addressToPublicKey(accounts[8], web3.eth.sign) + // @ts-ignore Broken typechain typing for bytes + await assertRevert(validators.updateEcdsaPublicKey(validator, signer, newPublicKey)) + }) + }) + }) + + describe('when not called by the registered `Accounts` contract', () => { + describe('when the public key matches the signer', () => { + let newPublicKey: string + const signer = accounts[9] + it('should revert', async () => { + newPublicKey = await addressToPublicKey(signer, web3.eth.sign) + // @ts-ignore Broken typechain typing for bytes + await assertRevert(validators.updateEcdsaPublicKey(validator, signer, newPublicKey)) + }) + }) + }) + }) + }) + + describe('#updateBlsPublicKey()', () => { + const newBlsPublicKey = web3.utils.randomHex(48) + const newBlsPoP = web3.utils.randomHex(96) describe('when called by a registered validator', () => { const validator = accounts[0] beforeEach(async () => { await registerValidator(validator) }) - describe('when the public keys data is the right length', () => { + describe('when the keys are the right length', () => { let resp: any beforeEach(async () => { // @ts-ignore Broken typechain typing for bytes - resp = await validators.updatePublicKeysData(newPublicKeysData) + resp = await validators.updateBlsPublicKey(newBlsPublicKey, newBlsPoP) }) - it('should set the validator public keys data', async () => { + it('should set the validator bls public key', async () => { const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.publicKeysData, newPublicKeysData) + assert.equal(parsedValidator.blsPublicKey, newBlsPublicKey) }) - it('should emit the ValidatorPublicKeysDataUpdated event', async () => { + it('should emit the ValidatorBlsPublicKeyUpdated event', async () => { assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { - event: 'ValidatorPublicKeysDataUpdated', + event: 'ValidatorBlsPublicKeyUpdated', args: { validator, - publicKeysData: newPublicKeysData, + blsPublicKey: newBlsPublicKey, }, }) }) }) - describe('when the public keys data is too long', () => { + describe('when the public key is not 48 bytes', () => { it('should revert', async () => { // @ts-ignore Broken typechain typing for bytes - await assertRevert(validators.updatePublicKeysData(newPublicKeysData + '00')) + await assertRevert(validators.updateBlsPublicKey(newBlsPublicKey + '01', newBlsPoP)) }) }) - describe('when the public keys data is too short', () => { + describe('when the proof of possession is not 96 bytes', () => { it('should revert', async () => { // @ts-ignore Broken typechain typing for bytes - await assertRevert(validators.updatePublicKeysData(newPublicKeysData.slice(0, -2))) + await assertRevert(validators.updateBlsPublicKey(newBlsPublicKey, newBlsPoP + '01')) }) }) }) @@ -1276,15 +1391,19 @@ contract('Validators', (accounts: string[]) => { await registerValidatorGroup(group) }) describe('when adding a validator affiliated with the group', () => { + let registrationEpoch: number beforeEach(async () => { await registerValidator(validator) + registrationEpoch = Math.floor((await web3.eth.getBlockNumber()) / EPOCH) await validators.affiliate(group, { from: validator }) }) describe('when the group meets the locked gold requirements', () => { describe('when the validator meets the locked gold requirements', () => { + let additionEpoch: number beforeEach(async () => { resp = await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS) + additionEpoch = Math.floor((await web3.eth.getBlockNumber()) / EPOCH) }) it('should add the member to the list of members', async () => { @@ -1306,14 +1425,14 @@ contract('Validators', (accounts: string[]) => { }) it("should update the member's membership history", async () => { - const membershipHistory = await validators.getMembershipHistory(validator) - const expectedEpoch = new BigNumber( - Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + const expectedEntries = registrationEpoch == additionEpoch ? 1 : 2 + const membershipHistory = parseMembershipHistory( + await validators.getMembershipHistory(validator) ) - assert.equal(membershipHistory[0].length, 1) - assertEqualBN(membershipHistory[0][0], expectedEpoch) - assert.equal(membershipHistory[1].length, 1) - assertSameAddress(membershipHistory[1][0], group) + assert.equal(membershipHistory.epochs.length, expectedEntries) + assertEqualBN(membershipHistory.epochs[expectedEntries - 1], additionEpoch) + assert.equal(membershipHistory.groups.length, expectedEntries) + assertSameAddress(membershipHistory.groups[expectedEntries - 1], group) }) it('should mark the group as eligible', async () => { @@ -1602,7 +1721,7 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('#updateValidatorScore', () => { + describe('#updateValidatorScoreFromSigner', () => { const validator = accounts[0] beforeEach(async () => { await registerValidator(validator) @@ -1614,7 +1733,7 @@ contract('Validators', (accounts: string[]) => { const epochScore = uptime.pow(validatorScoreParameters.exponent) const adjustmentSpeed = fromFixed(validatorScoreParameters.adjustmentSpeed) beforeEach(async () => { - await validators.updateValidatorScore(validator, toFixed(uptime)) + await validators.updateValidatorScoreFromSigner(validator, toFixed(uptime)) }) it('should update the validator score', async () => { @@ -1625,7 +1744,7 @@ contract('Validators', (accounts: string[]) => { describe('when the validator already has a non-zero score', () => { beforeEach(async () => { - await validators.updateValidatorScore(validator, toFixed(uptime)) + await validators.updateValidatorScoreFromSigner(validator, toFixed(uptime)) }) it('should update the validator score', async () => { @@ -1643,18 +1762,18 @@ contract('Validators', (accounts: string[]) => { describe('when uptime > 1.0', () => { const uptime = 1.01 it('should revert', async () => { - await assertRevert(validators.updateValidatorScore(validator, toFixed(uptime))) + await assertRevert(validators.updateValidatorScoreFromSigner(validator, toFixed(uptime))) }) }) }) describe('#updateMembershipHistory', () => { const validator = accounts[0] - const groups = accounts.slice(1) + const groups = accounts.slice(1, -1) let validatorRegistrationEpochNumber: number beforeEach(async () => { await registerValidator(validator) - const blockNumber = (await web3.eth.getBlock('latest')).number + const blockNumber = await web3.eth.getBlockNumber() validatorRegistrationEpochNumber = Math.floor(blockNumber / EPOCH) for (const group of groups) { await registerValidatorGroup(group) @@ -1668,7 +1787,7 @@ contract('Validators', (accounts: string[]) => { const expectedMembershipHistoryGroups = [NULL_ADDRESS] const expectedMembershipHistoryEpochs = [new BigNumber(validatorRegistrationEpochNumber)] for (let i = 0; i < numTests; i++) { - const blockNumber = (await web3.eth.getBlock('latest')).number + const blockNumber = await web3.eth.getBlockNumber() const epochNumber = Math.floor(blockNumber / EPOCH) const blocksUntilNextEpoch = (epochNumber + 1) * EPOCH - blockNumber await mineBlocks(blocksUntilNextEpoch, web3) @@ -1707,7 +1826,7 @@ contract('Validators', (accounts: string[]) => { const expectedMembershipHistoryGroups = [NULL_ADDRESS] const expectedMembershipHistoryEpochs = [new BigNumber(validatorRegistrationEpochNumber)] for (let i = 0; i < membershipHistoryLength.plus(1).toNumber(); i++) { - const blockNumber = (await web3.eth.getBlock('latest')).number + const blockNumber = await web3.eth.getBlockNumber() const epochNumber = Math.floor(blockNumber / EPOCH) const blocksUntilNextEpoch = (epochNumber + 1) * EPOCH - blockNumber await mineBlocks(blocksUntilNextEpoch, web3) @@ -1732,7 +1851,7 @@ contract('Validators', (accounts: string[]) => { describe('#getMembershipInLastEpoch', () => { const validator = accounts[0] - const groups = accounts.slice(1) + const groups = accounts.slice(1, -1) beforeEach(async () => { await registerValidator(validator) for (const group of groups) { @@ -1743,7 +1862,7 @@ contract('Validators', (accounts: string[]) => { describe('when changing groups more times than membership history length', () => { it('should always return the correct membership for the last epoch', async () => { for (let i = 0; i < membershipHistoryLength.plus(1).toNumber(); i++) { - const blockNumber = (await web3.eth.getBlock('latest')).number + const blockNumber = await web3.eth.getBlockNumber() const epochNumber = Math.floor(blockNumber / EPOCH) const blocksUntilNextEpoch = (epochNumber + 1) * EPOCH - blockNumber await mineBlocks(blocksUntilNextEpoch, web3) @@ -1835,7 +1954,7 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('#distributeEpochPayment', () => { + describe('#distributeEpochPaymentsFromSigner', () => { const validator = accounts[0] const group = accounts[1] const maxPayment = new BigNumber(20122394876) @@ -1844,6 +1963,10 @@ contract('Validators', (accounts: string[]) => { await registerValidatorGroupWithMembers(group, [validator]) mockStableToken = await MockStableToken.new() await registry.setAddressFor(CeloContractName.StableToken, mockStableToken.address) + // Fast-forward to the next epoch, so that the getMembershipInLastEpoch(validator) == group + const blockNumber = await web3.eth.getBlockNumber() + const epochNumber = Math.floor(blockNumber / EPOCH) + await mineBlocks((epochNumber + 1) * EPOCH - blockNumber, web3) }) describe('when the validator score is non-zero', () => { @@ -1857,14 +1980,15 @@ contract('Validators', (accounts: string[]) => { .times(fromFixed(commission)) .dp(0, BigNumber.ROUND_FLOOR) const expectedValidatorPayment = expectedTotalPayment.minus(expectedGroupPayment) + beforeEach(async () => { - await validators.updateValidatorScore(validator, toFixed(uptime)) + await validators.updateValidatorScoreFromSigner(validator, toFixed(uptime)) }) describe('when the validator and group meet the balance requirements', () => { beforeEach(async () => { - ret = await validators.distributeEpochPayment.call(validator, maxPayment) - await validators.distributeEpochPayment(validator, maxPayment) + ret = await validators.distributeEpochPaymentsFromSigner.call(validator, maxPayment) + await validators.distributeEpochPaymentsFromSigner(validator, maxPayment) }) it('should pay the validator', async () => { @@ -1886,8 +2010,8 @@ contract('Validators', (accounts: string[]) => { validator, validatorLockedGoldRequirements.value.minus(1) ) - ret = await validators.distributeEpochPayment.call(validator, maxPayment) - await validators.distributeEpochPayment(validator, maxPayment) + ret = await validators.distributeEpochPaymentsFromSigner.call(validator, maxPayment) + await validators.distributeEpochPaymentsFromSigner(validator, maxPayment) }) it('should not pay the validator', async () => { @@ -1909,8 +2033,8 @@ contract('Validators', (accounts: string[]) => { group, groupLockedGoldRequirements.value.minus(1) ) - ret = await validators.distributeEpochPayment.call(validator, maxPayment) - await validators.distributeEpochPayment(validator, maxPayment) + ret = await validators.distributeEpochPaymentsFromSigner.call(validator, maxPayment) + await validators.distributeEpochPaymentsFromSigner(validator, maxPayment) }) it('should not pay the validator', async () => { diff --git a/packages/protocol/test/identity/attestations.ts b/packages/protocol/test/identity/attestations.ts index ebed62fd29a..4add2b99b64 100644 --- a/packages/protocol/test/identity/attestations.ts +++ b/packages/protocol/test/identity/attestations.ts @@ -2,11 +2,11 @@ import Web3 = require('web3') import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { - advanceBlockNum, assertEqualBN, assertLogMatches2, assertRevert, assertSameAddress, + mineBlocks, NULL_ADDRESS, } from '@celo/protocol/lib/test-utils' import { attestToIdentifier } from '@celo/utils' @@ -26,6 +26,7 @@ import { MockRandomInstance, MockStableTokenContract, MockStableTokenInstance, + MockValidatorsContract, RegistryContract, RegistryInstance, TestAttestationsContract, @@ -43,6 +44,7 @@ const Attestations: TestAttestationsContract = artifacts.require('TestAttestatio const MockStableToken: MockStableTokenContract = artifacts.require('MockStableToken') const MockElection: MockElectionContract = artifacts.require('MockElection') const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') +const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') const Random: MockRandomContract = artifacts.require('MockRandom') const Registry: RegistryContract = artifacts.require('Registry') @@ -131,17 +133,21 @@ contract('Attestations', (accounts: string[]) => { accountsInstance = await Accounts.new() mockStableToken = await MockStableToken.new() otherMockStableToken = await MockStableToken.new() + const mockValidators = await MockValidators.new() attestations = await Attestations.new() random = await Random.new() random.addTestRandomness(0, '0x00') mockLockedGold = await MockLockedGold.new() + registry = await Registry.new() + await accountsInstance.initialize(registry.address) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) await Promise.all( accounts.map(async (account) => { await accountsInstance.createAccount({ from: account }) await unlockAndAuthorizeKey( KeyOffsets.VALIDATING_KEY_OFFSET, - accountsInstance.authorizeValidationSigner, + accountsInstance.authorizeValidatorSigner, account ) }) @@ -153,7 +159,6 @@ contract('Attestations', (accounts: string[]) => { privateKeyToAddress(getDerivedKey(KeyOffsets.VALIDATING_KEY_OFFSET, account)) ) ) - registry = await Registry.new() await registry.setAddressFor(CeloContractName.Accounts, accountsInstance.address) await registry.setAddressFor(CeloContractName.Random, random.address) await registry.setAddressFor(CeloContractName.Election, mockElection.address) @@ -348,7 +353,7 @@ contract('Attestations', (accounts: string[]) => { describe('when the original request has expired', () => { it('should allow to request more attestations', async () => { - await advanceBlockNum(attestationExpiryBlocks, web3) + await mineBlocks(attestationExpiryBlocks, web3) await attestations.request(phoneHash, 1, mockStableToken.address) }) }) @@ -480,7 +485,7 @@ contract('Attestations', (accounts: string[]) => { describe('after attestationExpiryBlocks', () => { beforeEach(async () => { await attestations.selectIssuers(phoneHash) - await advanceBlockNum(attestationExpiryBlocks, web3) + await mineBlocks(attestationExpiryBlocks, web3) }) it('should no longer list the attestations in getCompletableAttestations', async () => { @@ -551,7 +556,7 @@ contract('Attestations', (accounts: string[]) => { }) it('should set the time of the successful completion', async () => { - await advanceBlockNum(1, web3) + await mineBlocks(1, web3) await attestations.complete(phoneHash, v, r, s) const expectedBlock = await web3.eth.getBlock('latest') @@ -647,7 +652,7 @@ contract('Attestations', (accounts: string[]) => { }) it('does not let you verify beyond the window', async () => { - await advanceBlockNum(attestationExpiryBlocks, web3) + await mineBlocks(attestationExpiryBlocks, web3) await assertRevert(attestations.complete(phoneHash, v, r, s)) }) }) diff --git a/packages/utils/src/address.ts b/packages/utils/src/address.ts index dcaf9fd424b..92f846ebbf1 100644 --- a/packages/utils/src/address.ts +++ b/packages/utils/src/address.ts @@ -3,21 +3,47 @@ import { privateToAddress, privateToPublic, pubToAddress, toChecksumAddress } fr export type Address = string export function eqAddress(a: Address, b: Address) { - return a.replace('0x', '').toLowerCase() === b.replace('0x', '').toLowerCase() + return stripHexLeader(a).toLowerCase() === stripHexLeader(b).toLowerCase() } export const privateKeyToAddress = (privateKey: string) => { return toChecksumAddress( - '0x' + privateToAddress(Buffer.from(privateKey.slice(2), 'hex')).toString('hex') + ensureHexLeader( + privateToAddress(Buffer.from(stripHexLeader(privateKey), 'hex')).toString('hex') + ) + ) +} + +export const privateKeyToPublicKey = (privateKey: string) => { + return toChecksumAddress( + ensureHexLeader(privateToPublic(Buffer.from(stripHexLeader(privateKey), 'hex')).toString('hex')) ) } export const publicKeyToAddress = (publicKey: string) => { - return '0x' + pubToAddress(Buffer.from(publicKey.slice(2), 'hex')).toString('hex') + return toChecksumAddress( + ensureHexLeader(pubToAddress(Buffer.from(stripHexLeader(publicKey), 'hex')).toString('hex')) + ) } -export const privateKeyToPublicKey = (privateKey: string) => { - return '0x' + privateToPublic(Buffer.from(privateKey.slice(2), 'hex')).toString('hex') +/** + * Strips out the leading '0x' from a hex string. Does not fail on a string that does not + * contain a leading '0x' + * + * @param hexString Hex string that may have '0x' prepended to it. + * @returns hexString with no leading '0x'. + */ +export function stripHexLeader(hexString: string): string { + return hexString.indexOf('0x') === 0 ? hexString.slice(2) : hexString +} + +/** + * Returns a hex string with 0x prepended if it's not already starting with 0x + */ +export function ensureHexLeader(hexString: string): string { + return '0x' + stripHexLeader(hexString) } +export { isValidAddress } from 'ethereumjs-util' export { toChecksumAddress } from 'ethereumjs-util' +export { isValidChecksumAddress } from 'ethereumjs-util' diff --git a/packages/utils/src/bls.ts b/packages/utils/src/bls.ts index b6e3889ee36..287d69f6fe1 100644 --- a/packages/utils/src/bls.ts +++ b/packages/utils/src/bls.ts @@ -2,10 +2,14 @@ const keccak256 = require('keccak256') const BigInteger = require('bigi') const reverse = require('buffer-reverse') +import * as bls12377js from 'bls12377js' +import { isValidAddress } from './address' const n = BigInteger.fromHex('12ab655e9a2ca55660b44d1e5c37b00159aa76fed00000010a11800000000001', 16) const MODULUSMASK = 31 +export const BLS_PUBLIC_KEY_SIZE = 48 +export const BLS_POP_SIZE = 96 export const blsPrivateKeyToProcessedPrivateKey = (privateKeyHex: string) => { for (let i = 0; i < 256; i++) { @@ -35,3 +39,24 @@ export const blsPrivateKeyToProcessedPrivateKey = (privateKeyHex: string) => { throw new Error("couldn't derive BLS key from ECDSA key") } + +const getBlsPrivateKey = (privateKeyHex: string) => { + const blsPrivateKeyBytes = blsPrivateKeyToProcessedPrivateKey(privateKeyHex.slice(2)) + return blsPrivateKeyBytes +} + +export const getBlsPublicKey = (privateKeyHex: string) => { + const blsPrivateKeyBytes = getBlsPrivateKey(privateKeyHex) + return '0x' + bls12377js.BLS.privateToPublicBytes(blsPrivateKeyBytes).toString('hex') +} + +export const getBlsPoP = (address: string, privateKeyHex: string) => { + if (!isValidAddress(address)) { + throw new Error('Invalid checksum address for generating BLS proof-of-possession') + } + const blsPrivateKeyBytes = getBlsPrivateKey(privateKeyHex) + return ( + '0x' + + bls12377js.BLS.signPoP(blsPrivateKeyBytes, Buffer.from(address.slice(2), 'hex')).toString('hex') + ) +} diff --git a/packages/utils/src/io.ts b/packages/utils/src/io.ts index 2d01ee0573d..14708ff1ee8 100644 --- a/packages/utils/src/io.ts +++ b/packages/utils/src/io.ts @@ -1,8 +1,8 @@ import { isValidPublic, toChecksumAddress } from 'ethereumjs-util' import { either } from 'fp-ts/lib/Either' import * as t from 'io-ts' +import { isValidAddress } from './address' import { isE164NumberStrict } from './phoneNumbers' -import { isValidAddress } from './signatureUtils' // from http://urlregex.com/ export const URL_REGEX = new RegExp( diff --git a/packages/utils/src/signatureUtils.ts b/packages/utils/src/signatureUtils.ts index 177874535ed..e05f5427b7c 100644 --- a/packages/utils/src/signatureUtils.ts +++ b/packages/utils/src/signatureUtils.ts @@ -1,7 +1,9 @@ +import assert = require('assert') + const ethjsutil = require('ethereumjs-util') import * as Web3Utils from 'web3-utils' -import { privateKeyToAddress } from './address' +import { eqAddress, privateKeyToAddress } from './address' // If messages is a hex, the length of it should be the number of bytes function messageLength(message: string) { @@ -25,6 +27,30 @@ export interface Signer { sign: (message: string) => Promise } +export async function addressToPublicKey( + signer: string, + signFn: (message: string, signer: string) => Promise +) { + const msg = new Buffer('dummy_msg_data') + const data = '0x' + msg.toString('hex') + // Note: Eth.sign typing displays incorrect parameter order + const sig = await signFn(data, signer) + + const rawsig = ethjsutil.fromRpcSig(sig) + const prefixedMsg = hashMessageWithPrefix(data) + const pubKey = ethjsutil.ecrecover( + Buffer.from(prefixedMsg.slice(2), 'hex'), + rawsig.v, + rawsig.r, + rawsig.s + ) + + const computedAddr = ethjsutil.pubToAddress(pubKey).toString('hex') + assert(eqAddress(computedAddr, signer), 'computed address !== signer') + + return '0x' + pubKey.toString('hex') +} + // Uses a native function to sign (as signFn), most commonly `web.eth.sign` export function NativeSigner( signFn: (message: string, signer: string) => Promise, @@ -45,6 +71,16 @@ export function LocalSigner(privateKey: string): Signer { } } +export function signedMessageToPublicKey(message: string, v: number, r: string, s: string) { + const pubKeyBuf = ethjsutil.ecrecover( + Buffer.from(message.slice(2), 'hex'), + v, + Buffer.from(r.slice(2), 'hex'), + Buffer.from(s.slice(2), 'hex') + ) + return '0x' + pubKeyBuf.toString('hex') +} + export function signMessage(message: string, privateKey: string, address: string) { return signMessageWithoutPrefix(hashMessageWithPrefix(message), privateKey, address) } @@ -137,42 +173,6 @@ function isValidSignature(signer: string, message: string, v: number, r: string, } } -/** - * Strips out the leading '0x' from a hex string. Does not fail on a string that does not - * contain a leading '0x' - * - * @param hexString Hex string that may have '0x' prepended to it. - * @returns hexString with no leading '0x'. - */ -export function stripHexLeader(hexString: string): string { - return hexString.indexOf('0x') === 0 ? hexString.slice(2) : hexString -} - -/** - * Returns a hex string with 0x prepended if it's not already starting with 0x - */ -export function ensureHexLeader(hexString: string): string { - return '0x' + stripHexLeader(hexString) -} - -export function isValidAddress(address: string) { - return ( - typeof address === 'string' && - !ethjsutil.isZeroAddress(address) && - ethjsutil.isValidAddress(address) - ) -} - -export function areAddressesEqual(address1: string | null, address2: string | null) { - if (address1) { - address1 = stripHexLeader(address1.toLowerCase()) - } - if (address2) { - address2 = stripHexLeader(address2.toLowerCase()) - } - return address1 === address2 -} - export const SignatureUtils = { NativeSigner, LocalSigner, @@ -180,9 +180,5 @@ export const SignatureUtils = { signMessageWithoutPrefix, parseSignature, parseSignatureWithoutPrefix, - stripHexLeader, - ensureHexLeader, serializeSignature, - isValidAddress, - areAddressesEqual, }