diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index 96a80a87377..95c74c55063 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -7,15 +7,17 @@ import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import { bitIsSet, parseBlockExtraData } from '@celo/utils/lib/istanbul' import BigNumber from 'bignumber.js' import { assert } from 'chai' -import path from 'path' import Web3 from 'web3' -import { connectBipartiteClique, connectPeers, importGenesis, initAndStartGeth } from '../lib/geth' +import { AccountType, generateAddress, generatePrivateKey } from '../lib/generate_utils' +import { connectBipartiteClique, connectPeers, initAndStartGeth } from '../lib/geth' import { GethInstanceConfig } from '../lib/interfaces/geth-instance-config' import { GethRunConfig } from '../lib/interfaces/geth-run-config' import { assertAlmostEqual, getHooks, + mnemonic, sleep, + waitForAnnounceToStabilize, waitForBlock, waitForEpochTransition, waitToFinishInstanceSyncing, @@ -28,6 +30,14 @@ interface MemberSwapper { const TMP_PATH = '/tmp/e2e' const verbose = false const carbonOffsettingPartnerAddress = '0x1234567812345678123456781234567812345678' +const validatorAddress = '0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95' +// The tests calculate some expected values based on the rewards multiplier from the block +// before an epoch block. However, the actual rewards multiplier used for epoch rewards is +// calculated inside the epoch block. Since the multiplier depends on the timestamp, this means +// the expected values will never exactly match the actual values, so we need some tolerance. +// This constant defines the tolerance as a fraction of the expected value. +// values. We use 10^-6, so they have to be match to (nearly) 6 significant figures +const tolerance = new BigNumber(10).pow(new BigNumber(-6)) async function newMemberSwapper(kit: ContractKit, members: string[]): Promise { let index = 0 @@ -165,15 +175,15 @@ async function calculateUptime( // TODO(asa): Test independent rotation of ecdsa, bls keys. describe('governance tests', () => { const gethConfig: GethRunConfig = { - migrate: true, runPath: TMP_PATH, - verbosity: 4, - migrateTo: 25, + verbosity: 3, + useMycelo: true, networkId: 1101, network: 'local', genesisConfig: { churritoBlock: 0, donutBlock: 0, + epoch: 10, }, instances: [ // Validators 0 and 1 are swapped in and out of the group. @@ -214,11 +224,6 @@ describe('governance tests', () => { rpcport: 8553, }, ], - migrationOverrides: { - epochRewards: { - carbonOffsettingPartner: carbonOffsettingPartnerAddress, - }, - }, } const hooks: any = getHooks(gethConfig) @@ -229,7 +234,6 @@ describe('governance tests', () => { let sortedOracles: any let epochRewards: any let goldToken: any - let registry: any let reserve: any let validators: any let accounts: any @@ -250,16 +254,28 @@ describe('governance tests', () => { await hooks.restart() web3 = new Web3('http://localhost:8545') kit = newKitFromWeb3(web3) + // TODO(mcortesi): magic sleep. without it unlockAccount sometimes fails + await sleep(2) + // Assuming empty password + await kit.connection.web3.eth.personal.unlockAccount(validatorAddress, '', 1000000) goldToken = await kit._web3Contracts.getGoldToken() stableToken = await kit._web3Contracts.getStableToken() sortedOracles = await kit._web3Contracts.getSortedOracles() validators = await kit._web3Contracts.getValidators() - registry = await kit._web3Contracts.getRegistry() reserve = await kit._web3Contracts.getReserve() election = await kit._web3Contracts.getElection() epochRewards = await kit._web3Contracts.getEpochRewards() accounts = await kit._web3Contracts.getAccounts() + + await waitForBlock(web3, 1) + await waitForAnnounceToStabilize(web3) + + const er = await kit._web3Contracts.getEpochRewards() + const fraction = await er.methods.getCarbonOffsettingFraction().call() + await er.methods + .setCarbonOffsettingFund(carbonOffsettingPartnerAddress, fraction) + .send({ from: validatorAddress }) } const getValidatorGroupMembers = async (blockNumber?: number) => { @@ -288,6 +304,12 @@ describe('governance tests', () => { const getValidatorGroupPrivateKey = async () => { const [groupAddress] = await validators.methods.getRegisteredValidatorGroups().call() + // If we're using mycelo, we can just generate the validator group key directly + const myceloAddress = generateAddress(mnemonic, AccountType.VALIDATOR_GROUP, 0) + if (myceloAddress === groupAddress) { + return '0x' + generatePrivateKey(mnemonic, AccountType.VALIDATOR_GROUP, 0) + } + // Otherwise, the validator group key is encoded in its name (see 25_elect_validators.ts) const name = await accounts.methods.getName(groupAddress).call() const encryptedKeystore64 = name.split(' ')[1] const encryptedKeystore = JSON.parse(Buffer.from(encryptedKeystore64, 'base64').toString()) @@ -315,7 +337,8 @@ describe('governance tests', () => { ) assert.isFalse(currentBalance.isNaN()) assert.isFalse(previousBalance.isNaN()) - assertAlmostEqual(currentBalance.minus(previousBalance), expected) + const margin = expected.times(tolerance) + assertAlmostEqual(currentBalance.minus(previousBalance), expected, margin) } const assertTargetVotingYieldChanged = async (blockNumber: number, expected: BigNumber) => { @@ -407,7 +430,18 @@ describe('governance tests', () => { const groupKit = newKitFromWeb3(groupWeb3) const group: string = (await groupWeb3.eth.getAccounts())[0] + // Send some funds to the group, so it can afford fees + await ( + await kit.sendTransaction({ + from: validatorAddress, + to: group, + value: Web3.utils.toWei('1', 'ether'), + }) + ).waitReceipt() + // groupKit uses a different node than kit does, so wait a second in case kit's node + // got the new block before groupKit's node did. + await sleep(1) const txos = await (await groupKit.contracts.getElection()).activate(group) for (const txo of txos) { await txo.sendAndWaitForReceipt({ from: group }) @@ -416,6 +450,8 @@ describe('governance tests', () => { validators = await groupKit._web3Contracts.getValidators() const membersToSwap = [validatorAccounts[0], validatorAccounts[1]] const memberSwapper = await newMemberSwapper(groupKit, membersToSwap) + // The memberSwapper makes a change when it's created, so we wait for epoch change so it takes effect + await waitForEpochTransition(web3, epoch) const handled: any = {} @@ -659,7 +695,8 @@ describe('governance tests', () => { const previousVotes = new BigNumber( await election.methods.getTotalVotesForGroup(group).call({}, blockNumber - 1) ) - assertAlmostEqual(currentVotes.minus(previousVotes), expected) + const margin = expected.times(tolerance) + assertAlmostEqual(currentVotes.minus(previousVotes), expected, margin) } // Returns the gas fee base for a given block, which is distributed to the governance contract. @@ -1073,30 +1110,4 @@ describe('governance tests', () => { } }) }) - - describe('after the gold token smart contract is registered', () => { - let goldGenesisSupply = new BigNumber(0) - beforeEach(async function (this: any) { - this.timeout(0) // Disable test timeout - await restart() - const genesis = await importGenesis(path.join(gethConfig.runPath, 'genesis.json')) - Object.keys(genesis.alloc).forEach((address) => { - goldGenesisSupply = goldGenesisSupply.plus(genesis.alloc[address].balance) - }) - }) - - it('should initialize the Celo Gold total supply correctly', async function (this: any) { - const events = await registry.getPastEvents('RegistryUpdated', { fromBlock: 0 }) - let blockNumber = 0 - for (const e of events) { - if (e.returnValues.identifier === 'GoldToken') { - blockNumber = e.blockNumber - break - } - } - assert.isAtLeast(blockNumber, 1) - const goldTotalSupply = await goldToken.methods.totalSupply().call({}, blockNumber) - assert.equal(goldTotalSupply, goldGenesisSupply.toFixed()) - }) - }) }) diff --git a/packages/celotool/src/e2e-tests/replica_tests.ts b/packages/celotool/src/e2e-tests/replica_tests.ts index 87d2889c104..a168a9faadd 100644 --- a/packages/celotool/src/e2e-tests/replica_tests.ts +++ b/packages/celotool/src/e2e-tests/replica_tests.ts @@ -35,7 +35,7 @@ const verbose = false describe('replica swap tests', () => { const gethConfig: GethRunConfig = { - migrate: false, + useMycelo: true, runPath: TMP_PATH, verbosity: 4, networkId: 1101, diff --git a/packages/celotool/src/e2e-tests/slashing_tests.ts b/packages/celotool/src/e2e-tests/slashing_tests.ts index 07540a5c1b5..afd075c11ad 100644 --- a/packages/celotool/src/e2e-tests/slashing_tests.ts +++ b/packages/celotool/src/e2e-tests/slashing_tests.ts @@ -4,6 +4,7 @@ import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' import { ensureLeading0x } from '@celo/utils/lib/address' import BigNumber from 'bignumber.js' import { assert } from 'chai' +import * as _ from 'lodash' import * as rlp from 'rlp' import Web3 from 'web3' import { GethRunConfig } from '../lib/interfaces/geth-run-config' @@ -92,11 +93,11 @@ async function generateValidIntervalArrays( } describe('slashing tests', function (this: any) { - const gethConfigDown: GethRunConfig = { + const gethConfig: GethRunConfig = { network: 'local', networkId: 1101, runPath: TMP_PATH, - migrate: true, + useMycelo: true, genesisConfig: { churritoBlock: 0, donutBlock: 0, @@ -130,16 +131,6 @@ describe('slashing tests', function (this: any) { port: 30309, rpcport: 8551, }, - ], - } - - const gethConfig: GethRunConfig = { - network: 'local', - networkId: 1101, - runPath: TMP_PATH, - migrate: true, - instances: gethConfigDown.instances.concat([ - // Validator 4 will be down in the downtime test { name: 'validator4', validating: true, @@ -147,9 +138,14 @@ describe('slashing tests', function (this: any) { port: 30311, rpcport: 8553, }, - ]), + ], } + // Do a shallow copy so that the instance objects are the the same (even after the init step fills private keys, etc.) + const gethConfigDown = _.clone(gethConfig) + // Exclude the last validator to simulate it being down + gethConfigDown.instances = gethConfig.instances.slice(0, gethConfig.instances.length - 1) + const hooks: any = getHooks(gethConfig) const hooksDown: any = getHooks(gethConfigDown) let web3: Web3 @@ -157,7 +153,6 @@ describe('slashing tests', function (this: any) { before(async function (this: any) { this.timeout(0) - // Comment out the following line after a test run for a quick rerun. await hooks.before() }) @@ -230,6 +225,7 @@ describe('slashing tests', function (this: any) { this.timeout(0) // Disable test timeout const slasher = await kit._web3Contracts.getDowntimeSlasher() const slashableDowntime = new BigNumber(await slasher.methods.slashableDowntime().call()) + await waitForBlock(web3, 1) const blockNumber = await kit.connection.getBlockNumber() await waitForBlock(web3, blockNumber + slashableDowntime.toNumber() + 2 * safeMarginBlocks) @@ -237,7 +233,6 @@ describe('slashing tests', function (this: any) { doubleSigningBlock = await kit.connection.getBlock(blockNumber + 2 * safeMarginBlocks) const signer = await slasher.methods.validatorSignerAddressFromSet(4, blockNumber).call() - const validator = (await kit.connection.getAccounts())[0] await kit.connection.web3.eth.personal.unlockAccount(validator, '', 1000000) const lockedGold = await kit.contracts.getLockedGold() diff --git a/packages/celotool/src/e2e-tests/transfer_tests.ts b/packages/celotool/src/e2e-tests/transfer_tests.ts index f6425cd84cf..ce1133f6254 100644 --- a/packages/celotool/src/e2e-tests/transfer_tests.ts +++ b/packages/celotool/src/e2e-tests/transfer_tests.ts @@ -1,6 +1,6 @@ // tslint:disable-next-line: no-reference (Required to make this work w/ ts-node) import { CeloTxPending, CeloTxReceipt, TransactionResult } from '@celo/connect' -import { CeloContract, ContractKit, newKitFromWeb3 } from '@celo/contractkit' +import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' import { CeloTokenType, EachCeloToken, StableToken, Token } from '@celo/contractkit/lib/celo-tokens' import { eqAddress } from '@celo/utils/lib/address' import { toFixed } from '@celo/utils/lib/fixidity' @@ -157,22 +157,20 @@ describe('Transfer tests', function (this: any) { const TransferAmount: BigNumber = new BigNumber(Web3.utils.toWei('1', 'ether')) let currentGethInstance: GethInstanceConfig - const expectedProposerBlockReward: string = new BigNumber( - Web3.utils.toWei('1', 'ether') - ).toString() + let governanceAddress: string // set later on using the contract itself const validatorAddress = '0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95' const DEF_FROM_PK = 'f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d' const FromAddress = '0x5409ed021d9299bf6814279a6a1411a7e866a631' // Arbitrary addresses. - const governanceAddress = '0x00000000000000000000000000000000DeaDBeef' + const txFeeRecipientAddress = '0x5555555555555555555555555555555555555555' const ToAddress = '0xbBae99F0E1EE565404465638d40827b54D343638' - const FeeRecipientAddress = '0x4f5f8a3f45d179553e7b95119ce296010f50f6f1' + const gatewayFeeRecipientAddress = '0x4f5f8a3f45d179553e7b95119ce296010f50f6f1' const syncModes = ['full', 'fast', 'light', 'lightest'] const gethConfig: GethRunConfig = { - migrateTo: 20, + useMycelo: true, networkId: 1101, network: 'local', runPath: TMP_PATH, @@ -184,6 +182,9 @@ describe('Transfer tests', function (this: any) { { name: 'validator', validating: true, + minerValidator: validatorAddress, + // Separate address for tx fees, so that we can easy identify balance changes due to them + txFeeRecipient: txFeeRecipientAddress, syncmode: 'full', port: 30303, rpcport: 8545, @@ -214,8 +215,8 @@ describe('Transfer tests', function (this: any) { rpcport: 8547, // We need to set an etherbase here so that the full node will accept transactions from // light clients. - minerValidator: FeeRecipientAddress, - txFeeRecipient: FeeRecipientAddress, + minerValidator: gatewayFeeRecipientAddress, + txFeeRecipient: gatewayFeeRecipientAddress, } const restartWithCleanNodes = async () => { @@ -238,13 +239,17 @@ describe('Transfer tests', function (this: any) { 3 ) - // Install an arbitrary address as the goverance address to act as the infrastructure fund. - // This is chosen instead of full migration for speed and to avoid the need for a governance - // proposal, as all contracts are owned by governance once the migration is complete. - const registry = await kit._web3Contracts.getRegistry() - const tx = registry.methods.setAddressFor(CeloContract.Governance, governanceAddress) - const gas = await tx.estimateGas({ from: validatorAddress }) - await tx.send({ from: validatorAddress, gas }) + governanceAddress = (await kit._web3Contracts.getGovernance()).options.address + // The tests below check the balance of the governance contract (i.e. the community fund) + // before and after transactions to verify the correct amount has been received from the fees. + // This causes flakiness due to the fund also receiving epoch rewards (if the epoch change is + // between the blocks the balance checker uses as its before and after the test will fail due + // to the unexpected change from the epoch rewards). + // To avoid this, we set the community fund's fraction of epoch rewards to zero. + // Another option would have been to make the epoch size large enough so no epoch changes happen + // during the test. + const epochRewards = await kit._web3Contracts.getEpochRewards() + await epochRewards.methods.setCommunityRewardFraction(0).send({ from: validatorAddress }) // Give the account we will send transfers as sufficient gold and dollars. const startBalance = TransferAmount.times(500) @@ -490,8 +495,8 @@ describe('Transfer tests', function (this: any) { const accounts = [ fromAddress, toAddress, - validatorAddress, - FeeRecipientAddress, + txFeeRecipientAddress, + gatewayFeeRecipientAddress, governanceAddress, ] balances = await newBalanceWatcher(kit, accounts) @@ -570,19 +575,15 @@ describe('Transfer tests', function (this: any) { } it(`should increment the gateway fee recipient's ${feeToken} balance by the gateway fee`, () => - assertEqualBN(balances.delta(FeeRecipientAddress, feeToken), txRes.fees.gateway)) + assertEqualBN(balances.delta(gatewayFeeRecipientAddress, feeToken), txRes.fees.gateway)) it(`should increment the infrastructure fund's ${feeToken} balance by the base portion of the gas fee`, () => assertEqualBN(balances.delta(governanceAddress, feeToken), txRes.fees.base)) - it(`should increment the proposers's ${feeToken} balance by the rest of the gas fee`, () => { - assertEqualBN( - balances.delta(validatorAddress, feeToken).mod(expectedProposerBlockReward), - txRes.fees.tip - ) + it(`should increment the tx fee recipient's ${feeToken} balance by the rest of the gas fee`, () => { + assertEqualBN(balances.delta(txFeeRecipientAddress, feeToken), txRes.fees.tip) }) } - describe('Normal Transfer >', () => { before(restartWithCleanNodes) @@ -597,7 +598,7 @@ describe('Transfer tests', function (this: any) { const recipient = (choice: string) => { switch (choice) { case 'peer': - return FeeRecipientAddress + return gatewayFeeRecipientAddress case 'random': return Web3.utils.randomHex(20) default: @@ -800,8 +801,7 @@ describe('Transfer tests', function (this: any) { balances = await newBalanceWatcher(kit, [ FromAddress, ToAddress, - validatorAddress, - FeeRecipientAddress, + gatewayFeeRecipientAddress, governanceAddress, ]) @@ -845,8 +845,8 @@ describe('Transfer tests', function (this: any) { it("should halve the gateway fee recipient's Celo Dollar balance then increase it by the gateway fee", () => { assertEqualBN( balances - .current(FeeRecipientAddress, StableToken.cUSD) - .minus(balances.initial(FeeRecipientAddress, StableToken.cUSD).idiv(2)), + .current(gatewayFeeRecipientAddress, StableToken.cUSD) + .minus(balances.initial(gatewayFeeRecipientAddress, StableToken.cUSD).idiv(2)), expectedFees.gateway ) }) diff --git a/packages/celotool/src/e2e-tests/utils.ts b/packages/celotool/src/e2e-tests/utils.ts index eee1d798ae9..02db2ece41c 100644 --- a/packages/celotool/src/e2e-tests/utils.ts +++ b/packages/celotool/src/e2e-tests/utils.ts @@ -22,6 +22,7 @@ import { getEnodeAddress, getLogFilename, initAndStartGeth, + initGeth, migrateContracts, resetDataDir, restoreDatadir, @@ -102,6 +103,16 @@ export async function waitForEpochTransition(web3: Web3, epoch: number) { } while (blockNumber % epoch !== 1) } +export async function waitForAnnounceToStabilize(web3: Web3) { + // Due to a problem in the announce protocol's settings, it can take a minute for all the validators + // to be aware of each other even though they are connected. This can lead to the first validator missing + // block signatures initially. So we wait for that to pass. + // Before we used mycelo, this wasn't noticeable because the migrations meant that the network would have + // been running for close to 10 minutes already, which was more than enough time. + // TODO: This function and its uses can be removed after the announce startup behavior has been resolved. + await waitForBlock(web3, 70) +} + export function assertAlmostEqual( actual: BigNumber, expected: BigNumber, @@ -301,6 +312,15 @@ export function getContext(gethConfig: GethRunConfig, verbose: boolean = verbose } } + if (gethConfig.useMycelo || !(gethConfig.migrate || gethConfig.migrateTo)) { + // Just need to initialize the nodes in this case. No need to actually start the network + // since we don't need to run the migrations against it. + for (const instance of gethConfig.instances) { + await initGeth(gethConfig, gethBinaryPath, instance, verbose) + } + return + } + // Start all the instances for (const instance of gethConfig.instances) { await initAndStartGeth(gethConfig, gethBinaryPath, instance, verbose) @@ -309,20 +329,18 @@ export function getContext(gethConfig: GethRunConfig, verbose: boolean = verbose // Directly connect validator peers that are not using a bootnode or proxy. await connectValidatorPeers(gethConfig.instances) - if (!gethConfig.useMycelo && (gethConfig.migrate || gethConfig.migrateTo)) { - await Promise.all( - gethConfig.instances.filter((i) => i.validating).map((i) => waitToFinishInstanceSyncing(i)) - ) - - await migrateContracts( - MonorepoRoot, - validatorPrivateKeys, - attestationKeys, - validators.map((x) => x.address), - gethConfig.migrateTo, - gethConfig.migrationOverrides - ) - } + await Promise.all( + gethConfig.instances.filter((i) => i.validating).map((i) => waitToFinishInstanceSyncing(i)) + ) + + await migrateContracts( + MonorepoRoot, + validatorPrivateKeys, + attestationKeys, + validators.map((x) => x.address), + gethConfig.migrateTo, + gethConfig.migrationOverrides + ) } const before = async () => { diff --git a/packages/celotool/src/e2e-tests/validator_order_tests.ts b/packages/celotool/src/e2e-tests/validator_order_tests.ts index bac1288dff8..2e1e9097a76 100644 --- a/packages/celotool/src/e2e-tests/validator_order_tests.ts +++ b/packages/celotool/src/e2e-tests/validator_order_tests.ts @@ -16,7 +16,7 @@ describe('governance tests', () => { networkId: 1101, network: 'local', runPath: TMP_PATH, - migrateTo: 19, + useMycelo: true, instances: _.range(VALIDATORS).map((i) => ({ name: `validator${i}`, validating: true, diff --git a/packages/celotool/src/lib/geth.ts b/packages/celotool/src/lib/geth.ts index 1c79ed7aca6..700bad62f38 100644 --- a/packages/celotool/src/lib/geth.ts +++ b/packages/celotool/src/lib/geth.ts @@ -736,29 +736,20 @@ export async function initAndStartGeth( instance: GethInstanceConfig, verbose: boolean ) { - const datadir = getDatadir(gethConfig.runPath, instance) - - if (verbose) { - console.info(`geth:${instance.name}: init datadir ${datadir}`) - } - - const genesisPath = path.join(gethConfig.runPath, 'genesis.json') - await init(gethBinaryPath, datadir, genesisPath, verbose) - - if (instance.privateKey) { - await importPrivateKey(gethConfig, gethBinaryPath, instance, verbose) - } - + await initGeth(gethConfig, gethBinaryPath, instance, verbose) return startGeth(gethConfig, gethBinaryPath, instance, verbose) } -export async function init( +export async function initGeth( + gethConfig: GethRunConfig, gethBinaryPath: string, - datadir: string, - genesisPath: string, + instance: GethInstanceConfig, verbose: boolean ) { + const datadir = getDatadir(gethConfig.runPath, instance) + const genesisPath = path.join(gethConfig.runPath, 'genesis.json') if (verbose) { + console.info(`geth:${instance.name}: init datadir ${datadir}`) console.log(`init geth with genesis at ${genesisPath}`) } @@ -766,6 +757,9 @@ export async function init( await spawnCmdWithExitOnFailure(gethBinaryPath, ['--datadir', datadir, 'init', genesisPath], { silent: !verbose, }) + if (instance.privateKey) { + await importPrivateKey(gethConfig, gethBinaryPath, instance, verbose) + } } export async function importPrivateKey( @@ -895,8 +889,6 @@ export async function startGeth( if (instance.validating && !minerValidator) { throw new Error('miner.validator address from the instance is required') } - // TODO(ponti): add flag after Donut fork - // const txFeeRecipient = instance.txFeeRecipient || minerValidator const verbosity = gethConfig.verbosity ? gethConfig.verbosity : '3' instance.args = [ @@ -920,13 +912,8 @@ export async function startGeth( ] if (minerValidator) { - instance.args.push( - '--etherbase', // TODO(ponti): change to '--miner.validator' after deprecating the 'etherbase' flag - minerValidator - ) - // TODO(ponti): add flag after Donut fork - // '--tx-fee-recipient', - // txFeeRecipient + const txFeeRecipient = instance.txFeeRecipient || minerValidator + instance.args.push('--miner.validator', minerValidator, '--tx-fee-recipient', txFeeRecipient) } if (rpcport) {