import { ContractKit , newKitFromWeb3 } from '@celo/contractkit' ; import { eqAddress , privateKeyToAddress } from '@celo/utils/lib/address' ; import { concurrentMap } from '@celo/utils/lib/async' ; import { getBlsPoP , getBlsPublicKey } from '@celo/utils/lib/bls' ; 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 Web3 from 'web3' ; 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 , } from './utils' ; interface MemberSwapper { swap ( ) : Promise < void > ; } const TMP_PATH = '/tmp/e2e' ; const verbose = false ; const carbonOffsettingPartnerAddress = '0x1234567812345678123456781234567812345678' ; const validatorAddress = '0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95' ; const tolerance = new BigNumber ( 10 ) . pow ( new BigNumber ( - 6 ) ) ; async function newMemberSwapper ( kit , members ) { let index = 0 ; const group = ( await kit . connection . getAccounts ( ) ) [ 0 ] ; await Promise . all ( members . slice ( 1 ) . map ( ( member ) => removeMember ( member ) ) ) ; async function removeMember ( member ) { return ( await kit . contracts . getValidators ( ) ) . removeMember ( member ) . sendAndWaitForReceipt ( { from : group } ) ; } async function addMember ( member ) { return ( await ( await kit . contracts . getValidators ( ) ) . addMember ( group , member ) ) . sendAndWaitForReceipt ( { from : group } ) ; } async function getGroupMembers ( ) { const groupInfo = await ( await kit . _web3Contracts . getValidators ( ) ) . methods . getValidatorGroup ( group ) . call ( ) ; return groupInfo [ 0 ] ; } return { async swap ( ) { const removedMember = members [ index % members . length ] ; await removeMember ( members [ index % members . length ] ) ; index = index + 1 ; const addedMember = members [ index % members . length ] ; await addMember ( members [ index % members . length ] ) ; const groupMembers = await getGroupMembers ( ) ; assert . include ( groupMembers , addedMember ) ; assert . notInclude ( groupMembers , removedMember ) ; } , } ; } interface KeyRotator { rotate ( ) : Promise < void > ; } async function newKeyRotator ( kit , kits , privateKeys ) { let index = 0 ; const validator = ( await kit . connection . getAccounts ( ) ) [ 0 ] ; const accountsWrapper = await kit . contracts . getAccounts ( ) ; async function authorizeValidatorSigner ( signer , signerKit , signerPrivateKey ) { const blsPublicKey = getBlsPublicKey ( signerPrivateKey ) ; const blsPop = getBlsPoP ( validator , signerPrivateKey ) ; const accounts = await signerKit . contracts . getAccounts ( ) ; const pop = await accounts . generateProofOfKeyPossession ( validator , signer ) ; return ( await accountsWrapper . authorizeValidatorSignerAndBls ( signer , pop , blsPublicKey , blsPop ) ) . sendAndWaitForReceipt ( { from : validator , } ) ; } return { async rotate ( ) { if ( index < kits . length ) { const signerKit = kits [ index ] ; const signer = ( await signerKit . connection . getAccounts ( ) ) [ 0 ] ; const signerPrivateKey = privateKeys [ index ] ; await authorizeValidatorSigner ( signer , signerKit , signerPrivateKey ) ; index += 1 ; assert . equal ( await accountsWrapper . getValidatorSigner ( validator ) , signer ) ; } } , } ; } async function calculateUptime ( kit , validatorSetSize , lastBlockNumberOfEpoch , epochSize , lookbackWindow ) { const firstBlockNumberOfEpoch = lastBlockNumberOfEpoch - epochSize + 1 ; const monitoringWindow = [ firstBlockNumberOfEpoch + lookbackWindow - 1 , lastBlockNumberOfEpoch - 2 , ] ; const monitoringWindowSize = monitoringWindow [ 1 ] - monitoringWindow [ 0 ] + 1 ; const blocks = await concurrentMap ( 10 , [ ... Array ( monitoringWindowSize ) . keys ( ) ] , ( i ) => kit . connection . getBlock ( monitoringWindow [ 0 ] + 1 + i ) ) ; const lastSignedBlock = new Array ( validatorSetSize ) . fill ( 0 ) ; const upBlocks = new Array ( validatorSetSize ) . fill ( 0 ) ; for ( const block of blocks ) { const blockNumber = block . number - 1 ; const bitmap = parseBlockExtraData ( block . extraData ) . parentAggregatedSeal . bitmap ; const isMonitoredBlock = blockNumber >= monitoringWindow [ 0 ] && blockNumber <= monitoringWindow [ 1 ] ; const currentLookbackWindow = [ blockNumber - lookbackWindow + 1 , blockNumber ] ; for ( let signerIndex = 0 ; signerIndex < validatorSetSize ; signerIndex ++ ) { if ( bitIsSet ( bitmap , signerIndex ) ) { lastSignedBlock [ signerIndex ] = blockNumber ; } const lastSignedWithinLookbackwindow = lastSignedBlock [ signerIndex ] >= currentLookbackWindow [ 0 ] && lastSignedBlock [ signerIndex ] <= currentLookbackWindow [ 1 ] ; if ( isMonitoredBlock && lastSignedWithinLookbackwindow ) { upBlocks [ signerIndex ] ++ ; } } } const maxPotentialUpBlocks = monitoringWindowSize ; return upBlocks . map ( ( x ) => new BigNumber ( x / maxPotentialUpBlocks ) ) ; } describe ( 'governance tests' , ( ) => { const gethConfig = { runPath : TMP_PATH , verbosity : 3 , migrate : true , networkId : 1101 , network : 'local' , genesisConfig : { churritoBlock : 0 , donutBlock : 0 , epoch : 10 , } , instances : [ { name : 'validator0' , validating : true , syncmode : 'full' , port : 30303 , rpcport : 8545 , } , { name : 'validator1' , validating : true , syncmode : 'full' , port : 30305 , rpcport : 8547 , } , { 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 , } , ] , } ; const hooks = getHooks ( gethConfig ) ; let web3 ; let election ; let stableToken ; let sortedOracles ; let epochRewards ; let goldToken ; let reserve ; let validators ; let accounts ; let kit ; before ( async function ( this ) { this . timeout ( 0 ) ; await hooks . before ( ) ; } ) ; after ( async function ( this ) { this . timeout ( 0 ) ; await hooks . after ( ) ; } ) ; const restart = async ( ) => { await hooks . restart ( ) ; web3 = new Web3 ( 'http://localhost:8545' ) ; kit = newKitFromWeb3 ( web3 ) ; await sleep ( 2 ) ; 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 ( ) ; 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 ? ) => { if ( blockNumber ) { const [ groupAddress ] = await validators . methods . getRegisteredValidatorGroups ( ) . call ( { } , blockNumber ) ; const groupInfo = await validators . methods . getValidatorGroup ( groupAddress ) . call ( { } , blockNumber ) ; return groupInfo [ 0 ] ; } else { const [ groupAddress ] = await validators . methods . getRegisteredValidatorGroups ( ) . call ( ) ; const groupInfo = await validators . methods . getValidatorGroup ( groupAddress ) . call ( ) ; return groupInfo [ 0 ] ; } } ; const getValidatorSigner = async ( address , blockNumber ? ) => { 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 myceloAddress = generateAddress ( mnemonic , AccountType . VALIDATOR_GROUP , 0 ) ; if ( myceloAddress === groupAddress ) { return '0x' + generatePrivateKey ( mnemonic , AccountType . VALIDATOR_GROUP , 0 ) ; } const name = await accounts . methods . getName ( groupAddress ) . call ( ) ; const encryptedKeystore64 = name . split ( ' ' ) [ 1 ] ; const encryptedKeystore = JSON . parse ( Buffer . from ( encryptedKeystore64 , 'base64' ) . toString ( ) ) ; const encryptionKey = `0x${ gethConfig . instances [ 0 ] . privateKey }` ; const decryptedKeystore = web3 . eth . accounts . decrypt ( encryptedKeystore , encryptionKey ) ; return decryptedKeystore . privateKey ; } ; const isLastBlockOfEpoch = ( blockNumber , epochSize ) => blockNumber % epochSize === 0 ; const assertBalanceChanged = async ( address , blockNumber , expected , token ) => { const currentBalance = new BigNumber ( await token . methods . balanceOf ( address ) . call ( { } , blockNumber ) ) ; const previousBalance = new BigNumber ( await token . methods . balanceOf ( address ) . call ( { } , blockNumber - 1 ) ) ; assert . isFalse ( currentBalance . isNaN ( ) ) ; assert . isFalse ( previousBalance . isNaN ( ) ) ; const margin = expected . times ( tolerance ) ; assertAlmostEqual ( currentBalance . minus ( previousBalance ) , expected , margin ) ; } ; const assertTargetVotingYieldChanged = async ( blockNumber , expected ) => { const currentTarget = new BigNumber ( ( await epochRewards . methods . getTargetVotingYieldParameters ( ) . call ( { } , blockNumber ) ) [ 0 ] ) ; const previousTarget = new BigNumber ( ( await epochRewards . methods . getTargetVotingYieldParameters ( ) . call ( { } , blockNumber - 1 ) ) [ 0 ] ) ; const max = new BigNumber ( ( await epochRewards . methods . getTargetVotingYieldParameters ( ) . call ( { } , blockNumber ) ) [ 1 ] ) ; const expectedTarget = previousTarget . plus ( expected ) ; if ( expectedTarget . isGreaterThanOrEqualTo ( max ) ) { assert . equal ( currentTarget . toFixed ( ) , max . toFixed ( ) ) ; } else if ( expectedTarget . isLessThanOrEqualTo ( 0 ) ) { assert . isTrue ( currentTarget . isZero ( ) ) ; } else { const difference = currentTarget . minus ( previousTarget ) ; assert . equal ( fromFixed ( difference ) . dp ( 9 ) . toFixed ( ) , fromFixed ( expected ) . dp ( 9 ) . toFixed ( ) ) ; } } ; const assertTargetVotingYieldUnchanged = ( blockNumber ) => assertTargetVotingYieldChanged ( blockNumber , new BigNumber ( 0 ) ) ; const getLastEpochBlock = ( blockNumber , epoch ) => { const epochNumber = Math . floor ( ( blockNumber - 1 ) / epoch ) ; return epochNumber * epoch ; } ; const assertGoldTokenTotalSupplyUnchanged = ( blockNumber ) => assertGoldTokenTotalSupplyChanged ( blockNumber , new BigNumber ( 0 ) ) ; const assertGoldTokenTotalSupplyChanged = async ( blockNumber , expected ) => { const currentSupply = new BigNumber ( await goldToken . methods . totalSupply ( ) . call ( { } , blockNumber ) ) ; const previousSupply = new BigNumber ( await goldToken . methods . totalSupply ( ) . call ( { } , blockNumber - 1 ) ) ; assertAlmostEqual ( currentSupply . minus ( previousSupply ) , expected ) ; } ; describe ( 'when the validator set is changing' , ( ) => { const blockNumbers = [ ] ; let epoch ; let validatorAccounts ; before ( async function ( this ) { this . timeout ( 0 ) ; await restart ( ) ; const groupPrivateKey = await getValidatorGroupPrivateKey ( ) ; const validatorGroup = { name : 'validatorGroup' , validating : false , syncmode : 'full' , port : 30313 , wsport : 8555 , rpcport : 8557 , privateKey : groupPrivateKey . slice ( 2 ) , } ; await initAndStartGeth ( gethConfig , hooks . gethBinaryPath , validatorGroup , verbose ) ; await connectPeers ( [ ... gethConfig . instances , validatorGroup ] , verbose ) ; await waitToFinishInstanceSyncing ( validatorGroup ) ; validatorAccounts = await getValidatorGroupMembers ( ) ; assert . equal ( validatorAccounts . length , 5 ) ; epoch = new BigNumber ( await validators . methods . getEpochSize ( ) . call ( ) ) . toNumber ( ) ; assert . equal ( epoch , 10 ) ; await waitForEpochTransition ( web3 , epoch ) ; await sleep ( 5.5 ) ; await waitForEpochTransition ( web3 , epoch ) ; const groupWeb3Url = 'ws://localhost:8555' ; const groupWeb3 = new Web3 ( groupWeb3Url ) ; const groupKit = newKitFromWeb3 ( groupWeb3 ) ; const group = ( await groupWeb3 . eth . getAccounts ( ) ) [ 0 ] ; await ( await kit . sendTransaction ( { from : validatorAddress , to : group , value : Web3 . utils . toWei ( '1' , 'ether' ) , } ) ) . waitReceipt ( ) ; await sleep ( 1 ) ; const txos = await ( await groupKit . contracts . getElection ( ) ) . activate ( group ) ; for ( const txo of txos ) { await txo . sendAndWaitForReceipt ( { from : group } ) ; } validators = await groupKit . _web3Contracts . getValidators ( ) ; const membersToSwap = [ validatorAccounts [ 0 ] , validatorAccounts [ 1 ] ] ; const memberSwapper = await newMemberSwapper ( groupKit , membersToSwap ) ; await waitForEpochTransition ( web3 , epoch ) ; const handled = { } ; let errorWhileChangingValidatorSet = '' ; const changeValidatorSet = async ( header ) => { try { if ( handled [ header . number ] ) { return ; } handled [ header . number ] = true ; blockNumbers . push ( header . number ) ; if ( header . number % epoch === 0 && errorWhileChangingValidatorSet === '' ) { await memberSwapper . swap ( ) ; } } catch ( e ) { console . error ( e ) ; errorWhileChangingValidatorSet = e . toString ( ) ; } } ; const subscription = groupWeb3 . eth . subscribe ( 'newBlockHeaders' ) ; subscription . on ( 'data' , changeValidatorSet ) ; while ( blockNumbers . length < 40 ) { await sleep ( epoch ) ; } ; ( subscription as any ) . unsubscribe ( ) ; await sleep ( epoch ) ; assert . equal ( errorWhileChangingValidatorSet , '' ) ; } ) ; const getValidatorSetSignersAtBlock = async ( blockNumber ) : Promise < string [ ] > => { return election . methods . getCurrentValidatorSigners ( ) . call ( { } , blockNumber ) ; } ; const getValidatorSetAccountsAtBlock = async ( blockNumber ) => { const signingKeys = await getValidatorSetSignersAtBlock ( blockNumber ) ; return Promise . all ( signingKeys . map ( ( address ) => accounts . methods . signerToAccount ( address ) . call ( { } , blockNumber ) ) ) ; } ; it ( 'should always return a validator set size equal to the number of group members at the end of the last epoch' , async ( ) => { for ( const blockNumber of blockNumbers ) { const lastEpochBlock = getLastEpochBlock ( blockNumber , epoch ) ; const validatorSetSize = await election . methods . numberValidatorsInCurrentSet ( ) . call ( { } , blockNumber ) ; const groupMembership = await getValidatorGroupMembers ( lastEpochBlock ) ; assert . equal ( validatorSetSize , groupMembership . length ) ; } } ) ; 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 ) { this . timeout ( 0 ) ; for ( const blockNumber of blockNumbers ) { const lastEpochBlock = getLastEpochBlock ( blockNumber , epoch ) ; const memberAccounts = await getValidatorGroupMembers ( lastEpochBlock ) ; const memberSigners = await Promise . all ( memberAccounts . map ( ( v ) => getValidatorSigner ( v , lastEpochBlock ) ) ) ; const validatorSetSigners = await getValidatorSetSignersAtBlock ( blockNumber ) ; const validatorSetAccounts = await getValidatorSetAccountsAtBlock ( blockNumber ) ; assert . sameMembers ( memberSigners , validatorSetSigners ) ; assert . sameMembers ( memberAccounts , validatorSetAccounts ) ; } } ) ; it ( 'should block propose in a round robin fashion' , async ( ) => { let roundRobinOrder = [ ] ; for ( const blockNumber of blockNumbers ) { const lastEpochBlock = getLastEpochBlock ( blockNumber , 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 ( eqAddress ( block . miner , expectedProposer ) ) ; } } ) ; it ( 'should update the validator scores at the end of each epoch' , async function ( this ) { this . timeout ( 0 ) ; const scoreParams = await validators . methods . getValidatorScoreParameters ( ) . call ( ) ; const exponent = new BigNumber ( scoreParams [ 0 ] ) ; const adjustmentSpeed = fromFixed ( new BigNumber ( scoreParams [ 1 ] ) ) ; const assertScoreUnchanged = async ( validator , blockNumber ) => { const score = new BigNumber ( ( await validators . methods . getValidator ( validator ) . call ( { } , blockNumber ) ) . score ) ; const previousScore = new BigNumber ( ( await validators . methods . getValidator ( validator ) . call ( { } , blockNumber - 1 ) ) . score ) ; assert . isFalse ( score . isNaN ( ) ) ; assert . isFalse ( previousScore . isNaN ( ) ) ; assert . equal ( score . toFixed ( ) , previousScore . toFixed ( ) ) ; } ; const assertScoreChanged = async ( validator , blockNumber , uptime ) => { const score = new BigNumber ( ( await validators . methods . getValidator ( validator ) . call ( { } , blockNumber ) ) . score ) ; const previousScore = new BigNumber ( ( await validators . methods . getValidator ( validator ) . call ( { } , blockNumber - 1 ) ) . score ) ; assert . isFalse ( score . isNaN ( ) ) ; assert . isFalse ( previousScore . isNaN ( ) ) ; const epochScore = uptime . exponentiatedBy ( exponent ) ; const expectedScore = BigNumber . minimum ( epochScore , adjustmentSpeed . times ( epochScore ) . plus ( new BigNumber ( 1 ) . minus ( adjustmentSpeed ) . times ( fromFixed ( previousScore ) ) ) ) ; assertAlmostEqual ( score , toFixed ( expectedScore ) ) ; } ; for ( const blockNumber of blockNumbers ) { let expectUnchangedScores ; let expectChangedScores ; let electedValidators ; let uptime ; if ( isLastBlockOfEpoch ( blockNumber , epoch ) ) { expectChangedScores = await getValidatorSetAccountsAtBlock ( blockNumber ) ; expectUnchangedScores = validatorAccounts . filter ( ( x ) => ! expectChangedScores . includes ( x ) ) ; electedValidators = await getValidatorSetAccountsAtBlock ( blockNumber ) ; uptime = await calculateUptime ( kit , electedValidators . length , blockNumber , epoch , 3 ) ; } else { expectUnchangedScores = validatorAccounts ; expectChangedScores = [ ] ; electedValidators = [ ] ; uptime = [ ] ; } for ( const validator of expectUnchangedScores ) { await assertScoreUnchanged ( validator , blockNumber ) ; } for ( const validator of expectChangedScores ) { const signerIndex = electedValidators . map ( eqAddress . bind ( null , validator ) ) . indexOf ( true ) ; await assertScoreChanged ( validator , blockNumber , uptime [ signerIndex ] ) ; } } } ) ; it ( 'should distribute epoch payments at the end of each epoch' , async function ( this ) { this . timeout ( 0 ) ; const commission = 0.1 ; const targetValidatorEpochPayment = new BigNumber ( await epochRewards . methods . targetValidatorEpochPayment ( ) . call ( ) ) ; const [ group ] = await validators . methods . getRegisteredValidatorGroups ( ) . call ( ) ; const assertBalanceUnchanged = async ( validator , blockNumber ) => { await assertBalanceChanged ( validator , blockNumber , new BigNumber ( 0 ) , stableToken ) ; } ; const getExpectedTotalPayment = async ( validator , blockNumber ) => { const score = new BigNumber ( ( await validators . methods . getValidator ( validator ) . call ( { } , blockNumber ) ) . score ) ; assert . isFalse ( score . isNaN ( ) ) ; const rewardsMultiplier = new BigNumber ( await epochRewards . methods . getRewardsMultiplier ( ) . call ( { } , blockNumber - 1 ) ) ; return targetValidatorEpochPayment . times ( fromFixed ( score ) ) . times ( fromFixed ( rewardsMultiplier ) ) ; } ; for ( const blockNumber of blockNumbers ) { let expectUnchangedBalances ; let expectChangedBalances ; if ( isLastBlockOfEpoch ( blockNumber , epoch ) ) { expectChangedBalances = await getValidatorSetAccountsAtBlock ( blockNumber ) ; expectUnchangedBalances = validatorAccounts . filter ( ( x ) => ! expectChangedBalances . includes ( x ) ) ; } else { expectUnchangedBalances = validatorAccounts ; expectChangedBalances = [ ] ; } for ( const validator of expectUnchangedBalances ) { await assertBalanceUnchanged ( validator , blockNumber ) ; } let expectedGroupPayment = new BigNumber ( 0 ) ; for ( const validator of expectChangedBalances ) { const expectedTotalPayment = await getExpectedTotalPayment ( validator , blockNumber ) ; const groupPayment = expectedTotalPayment . times ( commission ) ; await assertBalanceChanged ( validator , blockNumber , expectedTotalPayment . minus ( groupPayment ) , stableToken ) ; expectedGroupPayment = expectedGroupPayment . plus ( groupPayment ) ; } await assertBalanceChanged ( group , blockNumber , expectedGroupPayment , stableToken ) ; } } ) ; it ( 'should distribute epoch rewards at the end of each epoch' , async function ( this ) { this . timeout ( 0 ) ; const lockedGold = await kit . _web3Contracts . getLockedGold ( ) ; const governance = await kit . _web3Contracts . getGovernance ( ) ; const gasPriceMinimum = await kit . _web3Contracts . getGasPriceMinimum ( ) ; const [ group ] = await validators . methods . getRegisteredValidatorGroups ( ) . call ( ) ; const assertVotesChanged = async ( blockNumber , expected ) => { const currentVotes = new BigNumber ( await election . methods . getTotalVotesForGroup ( group ) . call ( { } , blockNumber ) ) ; const previousVotes = new BigNumber ( await election . methods . getTotalVotesForGroup ( group ) . call ( { } , blockNumber - 1 ) ) ; const margin = expected . times ( tolerance ) ; assertAlmostEqual ( currentVotes . minus ( previousVotes ) , expected , margin ) ; } ; const blockBaseGasFee = async ( blockNumber ) : Promise < BigNumber > => { const gas = ( await web3 . eth . getBlock ( blockNumber ) ) . gasUsed ; const gpm = await gasPriceMinimum . methods . gasPriceMinimum ( ) . call ( { } , blockNumber ) ; return new BigNumber ( gpm ) . times ( new BigNumber ( gas ) ) ; } ; const assertLockedGoldBalanceChanged = async ( blockNumber , expected ) => { await assertBalanceChanged ( lockedGold . options . address , blockNumber , expected , goldToken ) ; } ; const assertGovernanceBalanceChanged = async ( blockNumber , expected ) => { await assertBalanceChanged ( governance . options . address , blockNumber , expected , goldToken ) ; } ; const assertReserveBalanceChanged = async ( blockNumber , expected ) => { await assertBalanceChanged ( reserve . options . address , blockNumber , expected , goldToken ) ; } ; const assertCarbonOffsettingBalanceChanged = async ( blockNumber , expected ) => { await assertBalanceChanged ( carbonOffsettingPartnerAddress , blockNumber , expected , goldToken ) ; } ; const assertVotesUnchanged = async ( blockNumber ) => { await assertVotesChanged ( blockNumber , new BigNumber ( 0 ) ) ; } ; const assertLockedGoldBalanceUnchanged = async ( blockNumber ) => { await assertLockedGoldBalanceChanged ( blockNumber , new BigNumber ( 0 ) ) ; } ; const assertReserveBalanceUnchanged = async ( blockNumber ) => { await assertReserveBalanceChanged ( blockNumber , new BigNumber ( 0 ) ) ; } ; const assertCarbonOffsettingBalanceUnchanged = async ( blockNumber ) => { await assertCarbonOffsettingBalanceChanged ( blockNumber , new BigNumber ( 0 ) ) ; } ; const getStableTokenSupplyChange = async ( blockNumber ) => { const currentSupply = new BigNumber ( await stableToken . methods . totalSupply ( ) . call ( { } , blockNumber ) ) ; const previousSupply = new BigNumber ( await stableToken . methods . totalSupply ( ) . call ( { } , blockNumber - 1 ) ) ; return currentSupply . minus ( previousSupply ) ; } ; const getStableTokenExchangeRate = async ( blockNumber ) => { const rate = await sortedOracles . methods . medianRate ( stableToken . options . address ) . call ( { } , blockNumber ) ; return new BigNumber ( rate [ 0 ] ) . div ( rate [ 1 ] ) ; } ; for ( const blockNumber of blockNumbers ) { if ( isLastBlockOfEpoch ( blockNumber , epoch ) ) { const activeVotes = new BigNumber ( await election . methods . getActiveVotes ( ) . call ( { } , blockNumber - 1 ) ) ; assert . isFalse ( activeVotes . isZero ( ) ) ; const rewardsMultiplier = new BigNumber ( await epochRewards . methods . getRewardsMultiplier ( ) . call ( { } , blockNumber - 1 ) ) ; assert . isFalse ( rewardsMultiplier . isZero ( ) ) ; const targetRewards = await epochRewards . methods . calculateTargetEpochRewards ( ) . call ( { } , blockNumber - 1 ) ; const perValidatorReward = new BigNumber ( targetRewards [ 0 ] ) ; const validatorSetSize = await election . methods . numberValidatorsInCurrentSet ( ) . call ( { } , blockNumber - 1 ) ; const exchangeRate = await getStableTokenExchangeRate ( blockNumber ) ; const maxPotentialValidatorReward = perValidatorReward . times ( validatorSetSize ) . div ( exchangeRate ) ; const targetVotingYield = new BigNumber ( ( await epochRewards . methods . getTargetVotingYieldParameters ( ) . call ( { } , blockNumber ) ) [ 0 ] ) ; assert . isFalse ( targetVotingYield . isZero ( ) ) ; const expectedVoterRewards = activeVotes . times ( fromFixed ( targetVotingYield ) ) . times ( fromFixed ( rewardsMultiplier ) ) ; const communityRewardFrac = new BigNumber ( await epochRewards . methods . getCommunityRewardFraction ( ) . call ( { } , blockNumber ) ) ; const carbonOffsettingFrac = new BigNumber ( await epochRewards . methods . getCarbonOffsettingFraction ( ) . call ( { } , blockNumber ) ) ; const fundFactor = new BigNumber ( 1 ) . minus ( fromFixed ( communityRewardFrac ) ) . minus ( fromFixed ( carbonOffsettingFrac ) ) ; const expectedCommunityReward = expectedVoterRewards . plus ( maxPotentialValidatorReward ) . times ( fromFixed ( communityRewardFrac ) ) . div ( fundFactor ) ; const expectedCarbonOffsettingPartnerAward = expectedVoterRewards . plus ( maxPotentialValidatorReward ) . times ( fromFixed ( carbonOffsettingFrac ) ) . div ( fundFactor ) ; const stableTokenSupplyChange = await getStableTokenSupplyChange ( blockNumber ) ; const expectedGoldTotalSupplyChange = expectedCommunityReward . plus ( expectedVoterRewards ) . plus ( expectedCarbonOffsettingPartnerAward ) . plus ( stableTokenSupplyChange . div ( exchangeRate ) ) ; const totalVoterRewards = new BigNumber ( targetRewards [ 1 ] ) ; const totalCommunityReward = new BigNumber ( targetRewards [ 2 ] ) ; const carbonOffsettingPartnerAward = new BigNumber ( targetRewards [ 3 ] ) ; assertAlmostEqual ( expectedVoterRewards , totalVoterRewards ) ; assertAlmostEqual ( expectedCommunityReward , totalCommunityReward ) ; assertAlmostEqual ( expectedCarbonOffsettingPartnerAward , carbonOffsettingPartnerAward ) ; await assertVotesChanged ( blockNumber , expectedVoterRewards ) ; await assertLockedGoldBalanceChanged ( blockNumber , expectedVoterRewards ) ; await assertGovernanceBalanceChanged ( blockNumber , expectedCommunityReward . plus ( await blockBaseGasFee ( blockNumber ) ) ) ; await assertReserveBalanceChanged ( blockNumber , stableTokenSupplyChange . div ( exchangeRate ) ) ; await assertGoldTokenTotalSupplyChanged ( blockNumber , expectedGoldTotalSupplyChange ) ; await assertCarbonOffsettingBalanceChanged ( blockNumber , expectedCarbonOffsettingPartnerAward ) ; } else { await assertVotesUnchanged ( blockNumber ) ; await assertGoldTokenTotalSupplyUnchanged ( blockNumber ) ; await assertLockedGoldBalanceUnchanged ( blockNumber ) ; await assertReserveBalanceUnchanged ( blockNumber ) ; await assertGovernanceBalanceChanged ( blockNumber , await blockBaseGasFee ( blockNumber ) ) ; await assertCarbonOffsettingBalanceUnchanged ( blockNumber ) ; } } } ) ; it ( 'should update the target voting yield' , async ( ) => { for ( const blockNumber of blockNumbers ) { if ( isLastBlockOfEpoch ( blockNumber , epoch ) ) { const votingGoldFraction = new BigNumber ( await epochRewards . methods . getVotingGoldFraction ( ) . call ( { } , blockNumber - 1 ) ) ; const targetVotingGoldFraction = new BigNumber ( await epochRewards . methods . getTargetVotingGoldFraction ( ) . call ( { } , blockNumber ) ) ; const difference = targetVotingGoldFraction . minus ( votingGoldFraction ) ; const adjustmentFactor = fromFixed ( new BigNumber ( ( await epochRewards . methods . getTargetVotingYieldParameters ( ) . call ( { } , blockNumber ) ) [ 2 ] ) ) ; const delta = difference . times ( adjustmentFactor ) ; await assertTargetVotingYieldChanged ( blockNumber , delta ) ; } else { await assertTargetVotingYieldUnchanged ( blockNumber ) ; } } } ) ; it ( 'should have emitted the correct events when paying epoch rewards' , async ( ) => { const currentBlock = ( await web3 . eth . getBlock ( 'latest' ) ) . number ; const events = [ { contract : epochRewards , name : 'TargetVotingYieldUpdated' , } , { contract : validators , name : 'ValidatorEpochPaymentDistributed' , } , { contract : validators , name : 'ValidatorScoreUpdated' , } , { contract : election , name : 'EpochRewardsDistributedToVoters' , } , ] ; for ( const event of events ) { const eventLogs = await event . contract . getPastEvents ( event . name , { fromBlock : currentBlock - 10 , currentBlock , } ) ; assert ( eventLogs . every ( ( a ) => a . blockNumber % 10 === 0 ) , `every ${ event . name } event occured on the last block of the epoch` ) ; assert ( eventLogs . length > 0 , `at least one ${ event . name } event occured` ) ; } } ) ; } ) ; describe ( 'when rotating keys' , ( ) => { const blockNumbers = [ ] ; const miners = [ ] ; const rotation0PrivateKey = '0xa42ac9c99f6ab2c96ee6cae1b40d36187f65cd878737f6623cd363fb94ba7087' ; const rotation1PrivateKey = '0x4519cae145fb9499358be484ca60c80d8f5b7f9c13ff82c88ec9e13283e9de1a' ; const rotation0Address = privateKeyToAddress ( rotation0PrivateKey ) ; const rotation1Address = privateKeyToAddress ( rotation1PrivateKey ) ; let epoch ; let validatorAccounts ; before ( async function ( this ) { this . timeout ( 0 ) ; await restart ( ) ; const groupPrivateKey = await getValidatorGroupPrivateKey ( ) ; const validatorGroup = { name : 'validatorGroup' , validating : false , syncmode : 'full' , port : 30313 , wsport : 8555 , rpcport : 8557 , privateKey : groupPrivateKey . slice ( 2 ) , } ; await initAndStartGeth ( gethConfig , hooks . gethBinaryPath , validatorGroup , verbose ) ; await connectPeers ( [ ... gethConfig . instances , validatorGroup ] , verbose ) ; console . log ( 'wait for validatorGroup to finish syncing' ) ; await waitToFinishInstanceSyncing ( validatorGroup ) ; const additionalValidatingNodes = [ { name : 'validator2KeyRotation0' , validating : true , syncmode : 'full' , lightserv : false , port : 30315 , wsport : 8559 , rpcport : 9559 , privateKey : rotation0PrivateKey . slice ( 2 ) , minerValidator : privateKeyToAddress ( rotation0PrivateKey . slice ( 2 ) ) , } , { name : 'validator2KeyRotation1' , validating : true , syncmode : 'full' , lightserv : false , port : 30317 , wsport : 8561 , rpcport : 9561 , privateKey : rotation1PrivateKey . slice ( 2 ) , minerValidator : privateKeyToAddress ( rotation1PrivateKey . slice ( 2 ) ) , } , ] ; await Promise . all ( additionalValidatingNodes . map ( ( nodeConfig ) => initAndStartGeth ( gethConfig , hooks . gethBinaryPath , nodeConfig , verbose ) ) ) ; await connectBipartiteClique ( gethConfig . instances , additionalValidatingNodes , verbose ) ; console . log ( 'wait for new validators to sync' ) ; await Promise . all ( additionalValidatingNodes . map ( ( i ) => waitToFinishInstanceSyncing ( i ) ) ) ; validatorAccounts = await getValidatorGroupMembers ( ) ; assert . equal ( validatorAccounts . length , 5 ) ; epoch = new BigNumber ( await validators . methods . getEpochSize ( ) . call ( ) ) . toNumber ( ) ; assert . equal ( epoch , 10 ) ; console . log ( 'wait for end of epoch' ) ; await waitForEpochTransition ( web3 , epoch ) ; const groupWeb3Url = 'ws://localhost:8555' ; const groupWeb3 = new Web3 ( groupWeb3Url ) ; const groupKit = newKitFromWeb3 ( groupWeb3 ) ; const validatorRpc = 'http://localhost:8549' ; const validatorWeb3 = new Web3 ( validatorRpc ) ; const authWeb31 = 'ws://localhost:8559' ; const authWeb32 = 'ws://localhost:8561' ; const authorizedKits = [ newKitFromWeb3 ( new Web3 ( authWeb31 ) ) , newKitFromWeb3 ( new Web3 ( authWeb32 ) ) , ] ; const authorizedPrivateKeys = [ rotation0PrivateKey , rotation1PrivateKey ] ; const keyRotator = await newKeyRotator ( newKitFromWeb3 ( validatorWeb3 ) , authorizedKits , authorizedPrivateKeys ) ; const handled = { } ; let errorWhileChangingValidatorSet = '' ; let lastRotated = 0 ; const changeValidatorSet = async ( header ) => { try { if ( handled [ header . number ] ) { return ; } handled [ header . number ] = true ; blockNumbers . push ( header . number ) ; miners . push ( header . miner ) ; if ( header . number % 10 === 0 && errorWhileChangingValidatorSet === '' && lastRotated + 60 <= header . number ) { lastRotated = header . number ; await keyRotator . rotate ( ) ; } } catch ( e ) { console . error ( e ) ; errorWhileChangingValidatorSet = e . toString ( ) ; } } ; const subscription = groupKit . connection . web3 . eth . subscribe ( 'newBlockHeaders' ) ; subscription . on ( 'data' , changeValidatorSet ) ; while ( blockNumbers . length < 180 ) { await sleep ( epoch ) ; } ; ( subscription as any ) . unsubscribe ( ) ; await sleep ( epoch ) ; assert . equal ( errorWhileChangingValidatorSet , '' ) ; } ) ; it ( 'validator 0 should have signed at least one block' , async ( ) => { const rotation0MinedBlock = miners . some ( ( a ) => eqAddress ( a , rotation0Address ) ) ; if ( ! rotation0MinedBlock ) { console . log ( rotation0Address , rotation1Address , miners ) ; } assert . isTrue ( rotation0MinedBlock ) ; } ) ; it ( 'validator 1 should have signed at least one block' , async ( ) => { const rotation1MinedBlock = miners . some ( ( a ) => eqAddress ( a , rotation1Address ) ) ; if ( ! rotation1MinedBlock ) { console . log ( rotation0Address , rotation1Address , miners ) ; } assert . isTrue ( rotation1MinedBlock ) ; } ) ; } ) ; describe ( 'when rewards distribution is frozen' , ( ) => { let epoch ; let blockFrozen ; let latestBlock ; before ( async function ( this ) { this . timeout ( 0 ) ; await restart ( ) ; const validator = ( await kit . connection . getAccounts ( ) ) [ 0 ] ; await kit . connection . web3 . eth . personal . unlockAccount ( validator , '' , 1000000 ) ; const freezer = await kit . _web3Contracts . getFreezer ( ) ; await freezer . methods . freeze ( epochRewards . options . address ) . send ( { from : validator } ) ; blockFrozen = await kit . connection . getBlockNumber ( ) ; epoch = new BigNumber ( await validators . methods . getEpochSize ( ) . call ( ) ) . toNumber ( ) ; await waitForBlock ( kit . connection . web3 , blockFrozen + epoch * 2 ) ; latestBlock = await kit . connection . getBlockNumber ( ) ; } ) ; it ( 'should not update the target voing yield' , async ( ) => { for ( let blockNumber = blockFrozen ; blockNumber < latestBlock ; blockNumber ++ ) { await assertTargetVotingYieldUnchanged ( blockNumber ) ; } } ) ; it ( 'should not mint new Celo Gold' , async ( ) => { for ( let blockNumber = blockFrozen ; blockNumber < latestBlock ; blockNumber ++ ) { await assertGoldTokenTotalSupplyUnchanged ( blockNumber ) ; } } ) ; } ) ; } ) ;