diff --git a/consensus/polybft/contractsapi/init.go b/consensus/polybft/contractsapi/init.go index aebae82f40..afe4469582 100644 --- a/consensus/polybft/contractsapi/init.go +++ b/consensus/polybft/contractsapi/init.go @@ -66,13 +66,14 @@ var ( // test smart contracts //go:embed test-contracts/* - testContracts embed.FS - TestWriteBlockMetadata *contracts.Artifact - RootERC20 *contracts.Artifact - TestSimple *contracts.Artifact - TestRewardToken *contracts.Artifact - Wrapper *contracts.Artifact - NumberPersister *contracts.Artifact + testContracts embed.FS + TestWriteBlockMetadata *contracts.Artifact + RootERC20 *contracts.Artifact + TestSimple *contracts.Artifact + TestRewardToken *contracts.Artifact + Wrapper *contracts.Artifact + NumberPersister *contracts.Artifact + TestValidatorSetPrecompile *contracts.Artifact contractArtifacts map[string]*contracts.Artifact ) @@ -270,6 +271,11 @@ func init() { log.Fatal(err) } + TestValidatorSetPrecompile, err = contracts.DecodeArtifact(readTestContractContent("TestValidatorSetPrecompile.json")) + if err != nil { + log.Fatal(err) + } + StakeManager, err = contracts.DecodeArtifact([]byte(StakeManagerArtifact)) if err != nil { log.Fatal(err) @@ -381,6 +387,7 @@ func init() { "RootERC20": RootERC20, "TestSimple": TestSimple, "TestRewardToken": TestRewardToken, + "TestValidatorSetPrecompile": TestValidatorSetPrecompile, } } diff --git a/consensus/polybft/contractsapi/test-contracts/TestValidatorSetPrecompile.json b/consensus/polybft/contractsapi/test-contracts/TestValidatorSetPrecompile.json new file mode 100644 index 0000000000..94fb9c4c27 --- /dev/null +++ b/consensus/polybft/contractsapi/test-contracts/TestValidatorSetPrecompile.json @@ -0,0 +1,66 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "TestValidatorSetPrecompile", + "sourceName": "consensus/polybft/contractsapi/test-contracts/TestValidatorSetContract.sol", + "abi": [ + { + "inputs":[ + + ], + "name":"VALIDATOR_SET_PRECOMPILE", + "outputs":[ + { + "internalType":"address", + "name":"", + "type":"address" + } + ], + "stateMutability":"view", + "type":"function" + }, + { + "inputs":[ + + ], + "name":"VALIDATOR_SET_PRECOMPILE_GAS", + "outputs":[ + { + "internalType":"uint256", + "name":"", + "type":"uint256" + } + ], + "stateMutability":"view", + "type":"function" + }, + { + "inputs":[ + + ], + "name":"hasQuorum", + "outputs":[ + { + "internalType":"bool", + "name":"", + "type":"bool" + } + ], + "stateMutability":"view", + "type":"function" + }, + { + "inputs":[ + + ], + "name":"inc", + "outputs":[ + + ], + "stateMutability":"nonpayable", + "type":"function" + } + ], + "bytecode": "0x608060405234801561000f575f80fd5b506106508061001d5f395ff3fe608060405234801561000f575f80fd5b5060043610610034575f3560e01c8063371303c014610038578063815b4d1b14610042575b5f80fd5b610040610060565b005b61004a610251565b6040516100579190610320565b60405180910390f35b5f8061204073ffffffffffffffffffffffffffffffffffffffff166203a980336040516020016100909190610378565b6040516020818303038152906040526040516100ac91906103fd565b5f604051808303818686fa925050503d805f81146100e5576040519150601f19603f3d011682016040523d82523d5f602084013e6100ea565b606091505b509150915081801561010c57508080602001905181019061010b9190610441565b5b61014b576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610142906104c6565b60405180910390fd5b5f803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f9054906101000a900460ff1661024d57600133908060018154018082558091505060019003905f5260205f20015f9091909190916101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060015f803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f6101000a81548160ff0219169083151502179055505b5050565b5f805f61204073ffffffffffffffffffffffffffffffffffffffff166203a980600160405160200161028391906105fa565b60405160208183030381529060405260405161029f91906103fd565b5f604051808303818686fa925050503d805f81146102d8576040519150601f19603f3d011682016040523d82523d5f602084013e6102dd565b606091505b50915091508180156102ff5750808060200190518101906102fe9190610441565b5b9250505090565b5f8115159050919050565b61031a81610306565b82525050565b5f6020820190506103335f830184610311565b92915050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61036282610339565b9050919050565b61037281610358565b82525050565b5f60208201905061038b5f830184610369565b92915050565b5f81519050919050565b5f81905092915050565b5f5b838110156103c25780820151818401526020810190506103a7565b5f8484015250505050565b5f6103d782610391565b6103e1818561039b565b93506103f18185602086016103a5565b80840191505092915050565b5f61040882846103cd565b915081905092915050565b5f80fd5b61042081610306565b811461042a575f80fd5b50565b5f8151905061043b81610417565b92915050565b5f6020828403121561045657610455610413565b5b5f6104638482850161042d565b91505092915050565b5f82825260208201905092915050565b7f76616c696461746f7200000000000000000000000000000000000000000000005f82015250565b5f6104b060098361046c565b91506104bb8261047c565b602082019050919050565b5f6020820190508181035f8301526104dd816104a4565b9050919050565b5f81549050919050565b5f82825260208201905092915050565b5f819050815f5260205f209050919050565b61051981610358565b82525050565b5f61052a8383610510565b60208301905092915050565b5f815f1c9050919050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61057261056d83610536565b610541565b9050919050565b5f6105848254610560565b9050919050565b5f600182019050919050565b5f6105a1826104e4565b6105ab81856104ee565b93506105b6836104fe565b805f5b838110156105ed576105ca82610579565b6105d4888261051f565b97506105df8361058b565b9250506001810190506105b9565b5085935050505092915050565b5f6020820190508181035f8301526106128184610597565b90509291505056fea26469706673582212206efd0d43fe03760dad6d6d6027319396055df00430d6654ecf53622a2dc85a4764736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {} +} \ No newline at end of file diff --git a/consensus/polybft/contractsapi/test-contracts/TestValidatorSetPrecompile.sol b/consensus/polybft/contractsapi/test-contracts/TestValidatorSetPrecompile.sol new file mode 100644 index 0000000000..a90c320ff2 --- /dev/null +++ b/consensus/polybft/contractsapi/test-contracts/TestValidatorSetPrecompile.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +// TestValidatorSetPrecompile.sol +// Contract which testst ValidatorSet precompile +pragma solidity ^0.8.0; + +contract TestValidatorSetPrecompile { + address constant VALIDATOR_SET_PRECOMPILE = 0x0000000000000000000000000000000000002040; + uint256 constant VALIDATOR_SET_PRECOMPILE_GAS = 240000; + + mapping(address => bool) voteMap; + address[] votes; + + modifier onlyValidator() { + (bool callSuccess, bytes memory returnData) = VALIDATOR_SET_PRECOMPILE.staticcall{ + gas: VALIDATOR_SET_PRECOMPILE_GAS + }(abi.encode(msg.sender)); + require(callSuccess && abi.decode(returnData, (bool)), "validator"); + _; + } + + function inc() public onlyValidator { + if (!voteMap[msg.sender]) { + votes.push(msg.sender); + voteMap[msg.sender] = true; + } + } + + function hasQuorum() public view returns (bool) { + (bool callSuccess, bytes memory returnData) = VALIDATOR_SET_PRECOMPILE.staticcall{ + gas: VALIDATOR_SET_PRECOMPILE_GAS + }(abi.encode(votes)); + return callSuccess && abi.decode(returnData, (bool)); + } +} diff --git a/consensus/polybft/polybft.go b/consensus/polybft/polybft.go index a9f2d5328d..1f6b87df68 100644 --- a/consensus/polybft/polybft.go +++ b/consensus/polybft/polybft.go @@ -471,6 +471,9 @@ func (p *Polybft) Initialize() error { executor: p.config.Executor, } + // enable validatorset precompile + p.config.Executor.SetValidatorSetBackend(p) + // create bridge and consensus topics if err = p.createTopics(); err != nil { return fmt.Errorf("cannot create topics: %w", err) @@ -754,6 +757,10 @@ func (p *Polybft) GetValidators(blockNumber uint64, parents []*types.Header) (va return p.validatorsCache.GetSnapshot(blockNumber, parents, nil) } +func (p *Polybft) GetValidatorsForBlock(blockNumber uint64) (validator.AccountSet, error) { + return p.validatorsCache.GetSnapshot(blockNumber, nil, nil) +} + func (p *Polybft) GetValidatorsWithTx(blockNumber uint64, parents []*types.Header, dbTx *bolt.Tx) (validator.AccountSet, error) { return p.validatorsCache.GetSnapshot(blockNumber, parents, dbTx) @@ -819,6 +826,20 @@ func (p *Polybft) GetLatestChainConfig() (*chain.Params, error) { return nil, nil } +func (p *Polybft) GetMaxValidatorSetSize() (uint64, error) { + params, err := p.GetLatestChainConfig() + if err != nil { + return 0, err + } + + polyBFTConfig, err := GetPolyBFTConfig(params) + if err != nil { + return 0, err + } + + return polyBFTConfig.MaxValidatorSetSize, nil +} + // GetBridgeProvider is an implementation of Consensus interface // Returns an instance of BridgeDataProvider func (p *Polybft) GetBridgeProvider() consensus.BridgeDataProvider { diff --git a/consensus/polybft/validators_snapshot.go b/consensus/polybft/validators_snapshot.go index 8d8fecf7fa..1a6e0b15cc 100644 --- a/consensus/polybft/validators_snapshot.go +++ b/consensus/polybft/validators_snapshot.go @@ -182,15 +182,13 @@ func (v *validatorsSnapshotCache) computeSnapshot( v.logger.Trace("Compute snapshot started...", "BlockNumber", nextEpochEndBlockNumber) - if len(parents) > 0 { - for i := len(parents) - 1; i >= 0; i-- { - parentHeader := parents[i] - if parentHeader.Number == nextEpochEndBlockNumber { - v.logger.Trace("Compute snapshot. Found header in parents", "Header", parentHeader.Number) - header = parentHeader - - break - } + for i := len(parents) - 1; i >= 0; i-- { + parentHeader := parents[i] + if parentHeader.Number == nextEpochEndBlockNumber { + v.logger.Trace("Compute snapshot. Found header in parents", "Header", parentHeader.Number) + header = parentHeader + + break } } diff --git a/contracts/system_addresses.go b/contracts/system_addresses.go index 1e8bf3daf6..b5d27eddf4 100644 --- a/contracts/system_addresses.go +++ b/contracts/system_addresses.go @@ -95,8 +95,10 @@ var ( // NativeTransferPrecompile is an address of native transfer precompile NativeTransferPrecompile = types.StringToAddress("0x2020") - // BLSAggSigsVerificationPrecompile is an address of BLS aggregated signatures verificatin precompile + // BLSAggSigsVerificationPrecompile is an address of BLS aggregated signatures verification precompile BLSAggSigsVerificationPrecompile = types.StringToAddress("0x2030") + // ValidatorSetPrecompile is an address of precompile which provides some validatorSet functionalities + ValidatorSetPrecompile = types.StringToAddress("0x2040") // ConsolePrecompile is and address of Hardhat console precompile ConsolePrecompile = types.StringToAddress("0x000000000000000000636F6e736F6c652e6c6f67") // AllowListContractsAddr is the address of the contract deployer allow list diff --git a/e2e-polybft/e2e/consensus_test.go b/e2e-polybft/e2e/consensus_test.go index 0115d6bcb1..2941b5322d 100644 --- a/e2e-polybft/e2e/consensus_test.go +++ b/e2e-polybft/e2e/consensus_test.go @@ -823,3 +823,119 @@ func TestE2E_Deploy_Nested_Contract(t *testing.T) { require.Equal(t, numberToPersist, parsedResponse) } + +func TestE2E_TestValidatorSetPrecompile(t *testing.T) { + var ( + premineBalance = ethgo.Ether(2e6) // 2M native tokens (so that we have enough balance to fund new validator) + stakeAmount = ethgo.Ether(500) + ) + + admin, err := wallet.GenerateKey() + require.NoError(t, err) + + dummyKey, err := wallet.GenerateKey() + require.NoError(t, err) + + // start cluster with 'validatorSize' validators + cluster := framework.NewTestCluster(t, 4, + framework.WithBladeAdmin(admin.Address().String()), + framework.WithSecretsCallback(func(addresses []types.Address, config *framework.TestClusterConfig) { + for _, a := range addresses { + config.Premine = append(config.Premine, fmt.Sprintf("%s:%s", a, premineBalance)) + config.StakeAmounts = append(config.StakeAmounts, stakeAmount) + } + + config.Premine = append(config.Premine, fmt.Sprintf("%s:%s", dummyKey.Address(), premineBalance)) + }), + ) + + defer cluster.Stop() + + cluster.WaitForReady(t) + + validatorKeys := make([]*wallet.Key, len(cluster.Servers)) + + for i, s := range cluster.Servers { + voterAcc, err := validatorHelper.GetAccountFromDir(s.DataDir()) + require.NoError(t, err) + + validatorKeys[i] = voterAcc.Ecdsa + } + + txRelayer, err := txrelayer.NewTxRelayer(txrelayer.WithClient(cluster.Servers[0].JSONRPC())) + require.NoError(t, err) + + // deploy contract + receipt, err := txRelayer.SendTransaction( + ðgo.Transaction{ + To: nil, + Input: contractsapi.TestValidatorSetPrecompile.Bytecode, + }, + admin) + require.NoError(t, err) + + validatorSetPrecompileTestAddr := receipt.ContractAddress + + hasQuorum := func() bool { + t.Helper() + + hasQuorumFn := contractsapi.TestValidatorSetPrecompile.Abi.GetMethod("hasQuorum") + + hasQuorumFnBytes, err := hasQuorumFn.Encode([]interface{}{}) + require.NoError(t, err) + + response, err := txRelayer.Call(ethgo.ZeroAddress, validatorSetPrecompileTestAddr, hasQuorumFnBytes) + require.NoError(t, err) + + return response == "0x0000000000000000000000000000000000000000000000000000000000000001" + } + + sendIncTx := func(validatorID int) { + t.Helper() + + isValidator := validatorID >= 0 && validatorID < len(validatorKeys) + incFn := contractsapi.TestValidatorSetPrecompile.Abi.GetMethod("inc") + + incFnBytes, err := incFn.Encode([]interface{}{}) + require.NoError(t, err) + + var key *wallet.Key + if isValidator { + key = validatorKeys[validatorID] + } else { + key = dummyKey + } + + txn := ðgo.Transaction{ + From: key.Address(), + To: &validatorSetPrecompileTestAddr, + Input: incFnBytes, + } + + receipt, err = txRelayer.SendTransaction(txn, key) + + if isValidator { + require.NoError(t, err) + require.Equal(t, uint64(types.ReceiptSuccess), receipt.Status) + } else { + require.ErrorContains(t, err, "unable to apply transaction even for the highest gas limit") + } + } + + require.False(t, hasQuorum()) + + sendIncTx(0) + require.False(t, hasQuorum()) + + sendIncTx(1) + require.False(t, hasQuorum()) + + sendIncTx(1) + require.False(t, hasQuorum()) + + sendIncTx(-1) // non validator + require.False(t, hasQuorum()) + + sendIncTx(3) + require.True(t, hasQuorum()) +} diff --git a/helper/predeployment/predeployment.go b/helper/predeployment/predeployment.go index ed152d7de5..324186f386 100644 --- a/helper/predeployment/predeployment.go +++ b/helper/predeployment/predeployment.go @@ -68,7 +68,7 @@ func getPredeployAccount(address types.Address, input []byte, config := chain.AllForksEnabled.At(0) // Create a transition - transition := state.NewTransition(hclog.NewNullLogger(), config, snapshot, radix) + transition := state.NewTransition(hclog.NewNullLogger(), config, snapshot, radix, nil) transition.ContextPtr().ChainID = chainID // Run the transition through the EVM diff --git a/state/executor.go b/state/executor.go index 4f094c0c86..39bb4d77ff 100644 --- a/state/executor.go +++ b/state/executor.go @@ -45,6 +45,10 @@ type Executor struct { PostHook func(txn *Transition) GenesisPostHook func(*Transition) error + + // this value should be set if we want to enable validator set precompile + // note that this precompile WONT be enabled for WriteGenesis + validatorSetBackend precompiled.ValidatorSetPrecompileBackend } // NewExecutor creates a new executor @@ -81,7 +85,7 @@ func (e *Executor) WriteGenesis( ChainID: e.config.ChainID, } - transition := NewTransition(e.logger, config, snap, txn) + transition := NewTransition(e.logger, config, snap, txn, nil) transition.ctx = env for addr, account := range alloc { @@ -199,7 +203,7 @@ func (e *Executor) BeginTxn( BurnContract: burnContract, } - t := NewTransition(e.logger, forkConfig, snap, newTxn) + t := NewTransition(e.logger, forkConfig, snap, newTxn, e.validatorSetBackend) t.PostHook = e.PostHook t.getHash = e.GetHash(header) t.ctx = txCtx @@ -235,6 +239,10 @@ func (e *Executor) BeginTxn( return t, nil } +func (e *Executor) SetValidatorSetBackend(validatorSetBackend precompiled.ValidatorSetPrecompileBackend) { + e.validatorSetBackend = validatorSetBackend +} + type Transition struct { logger hclog.Logger @@ -272,14 +280,15 @@ type Transition struct { accessList *runtime.AccessList } -func NewTransition(logger hclog.Logger, config chain.ForksInTime, snap Snapshot, radix *Txn) *Transition { +func NewTransition(logger hclog.Logger, config chain.ForksInTime, + snap Snapshot, radix *Txn, validatorSetBackend precompiled.ValidatorSetPrecompileBackend) *Transition { return &Transition{ logger: logger, config: config, state: radix, snap: snap, evm: evm.NewEVM(), - precompiles: precompiled.NewPrecompiled(), + precompiles: precompiled.NewPrecompiled(validatorSetBackend), journal: &runtime.Journal{}, accessList: runtime.NewAccessList(), } diff --git a/state/executor_test.go b/state/executor_test.go index cb04b09b2b..ce2183c7bc 100644 --- a/state/executor_test.go +++ b/state/executor_test.go @@ -37,7 +37,7 @@ func TestOverride(t *testing.T) { balance := big.NewInt(2) code := []byte{0x1} - tt := NewTransition(hclog.NewNullLogger(), chain.ForksInTime{}, state, newTxn(state)) + tt := NewTransition(hclog.NewNullLogger(), chain.ForksInTime{}, state, newTxn(state), nil) require.Empty(t, tt.state.GetCode(types.ZeroAddress)) @@ -256,7 +256,7 @@ func Test_Transition_EIP2929(t *testing.T) { txn.SetCode(addr, tt.code) enabledForks := chain.AllForksEnabled.At(0) - transition := NewTransition(hclog.NewNullLogger(), enabledForks, state, txn) + transition := NewTransition(hclog.NewNullLogger(), enabledForks, state, txn, nil) initialAccessList := runtime.NewAccessList() initialAccessList.PrepareAccessList(transition.ctx.Origin, &addr, transition.precompiles.Addrs, nil) transition.accessList = initialAccessList diff --git a/state/runtime/precompiled/base.go b/state/runtime/precompiled/base.go index 9997d4ebdc..3d081ce2d8 100644 --- a/state/runtime/precompiled/base.go +++ b/state/runtime/precompiled/base.go @@ -17,7 +17,7 @@ type ecrecover struct { p *Precompiled } -func (e *ecrecover) gas(input []byte, config *chain.ForksInTime) uint64 { +func (e *ecrecover) gas(_ []byte, _ types.Address, _ *chain.ForksInTime) uint64 { return 3000 } @@ -57,7 +57,7 @@ func (e *ecrecover) run(input []byte, caller types.Address, _ runtime.Host) ([]b type identity struct { } -func (i *identity) gas(input []byte, config *chain.ForksInTime) uint64 { +func (i *identity) gas(input []byte, _ types.Address, _ *chain.ForksInTime) uint64 { return baseGasCalc(input, 15, 3) } @@ -68,7 +68,7 @@ func (i *identity) run(input []byte, _ types.Address, _ runtime.Host) ([]byte, e type sha256h struct { } -func (s *sha256h) gas(input []byte, config *chain.ForksInTime) uint64 { +func (s *sha256h) gas(input []byte, _ types.Address, _ *chain.ForksInTime) uint64 { return baseGasCalc(input, 60, 12) } @@ -82,7 +82,7 @@ type ripemd160h struct { p *Precompiled } -func (r *ripemd160h) gas(input []byte, config *chain.ForksInTime) uint64 { +func (r *ripemd160h) gas(input []byte, _ types.Address, _ *chain.ForksInTime) uint64 { return baseGasCalc(input, 600, 120) } diff --git a/state/runtime/precompiled/base_test.go b/state/runtime/precompiled/base_test.go index dc2aeada73..a548d19345 100644 --- a/state/runtime/precompiled/base_test.go +++ b/state/runtime/precompiled/base_test.go @@ -25,7 +25,7 @@ func testPrecompiled(t *testing.T, p contract, cases []precompiledTest, enabledF h, _ := hex.DecodeString(c.Input) if c.Gas != 0 && len(enabledForks) > 0 { - gas := p.gas(h, enabledForks[0]) + gas := p.gas(h, types.Address{}, enabledForks[0]) assert.Equal(t, c.Gas, gas, "Incorrect gas estimation") } diff --git a/state/runtime/precompiled/blake2f.go b/state/runtime/precompiled/blake2f.go index 62ad2872b1..a54f0e88bd 100644 --- a/state/runtime/precompiled/blake2f.go +++ b/state/runtime/precompiled/blake2f.go @@ -14,7 +14,7 @@ type blake2f struct { p *Precompiled } -func (e *blake2f) gas(input []byte, config *chain.ForksInTime) uint64 { +func (e *blake2f) gas(input []byte, _ types.Address, _ *chain.ForksInTime) uint64 { if len(input) != 213 { return 0 } diff --git a/state/runtime/precompiled/bls_agg_sigs_verification.go b/state/runtime/precompiled/bls_agg_sigs_verification.go index 06e21cfc61..d343b1d962 100644 --- a/state/runtime/precompiled/bls_agg_sigs_verification.go +++ b/state/runtime/precompiled/bls_agg_sigs_verification.go @@ -32,7 +32,7 @@ type blsAggSignsVerification struct { } // gas returns the gas required to execute the pre-compiled contract -func (c *blsAggSignsVerification) gas(input []byte, _ *chain.ForksInTime) uint64 { +func (c *blsAggSignsVerification) gas(_ []byte, _ types.Address, _ *chain.ForksInTime) uint64 { return 150000 } diff --git a/state/runtime/precompiled/bn256.go b/state/runtime/precompiled/bn256.go index 7181a3a647..07c3d4e38b 100644 --- a/state/runtime/precompiled/bn256.go +++ b/state/runtime/precompiled/bn256.go @@ -14,7 +14,7 @@ type bn256Add struct { p *Precompiled } -func (b *bn256Add) gas(input []byte, config *chain.ForksInTime) uint64 { +func (b *bn256Add) gas(_ []byte, _ types.Address, config *chain.ForksInTime) uint64 { if config.Istanbul { return 150 } @@ -48,7 +48,7 @@ type bn256Mul struct { p *Precompiled } -func (b *bn256Mul) gas(input []byte, config *chain.ForksInTime) uint64 { +func (b *bn256Mul) gas(_ []byte, _ types.Address, config *chain.ForksInTime) uint64 { if config.Istanbul { return 6000 } @@ -79,7 +79,7 @@ type bn256Pairing struct { p *Precompiled } -func (b *bn256Pairing) gas(input []byte, config *chain.ForksInTime) uint64 { +func (b *bn256Pairing) gas(input []byte, _ types.Address, config *chain.ForksInTime) uint64 { baseGas, pointGas := uint64(100000), uint64(80000) if config.Istanbul { baseGas, pointGas = 45000, 34000 diff --git a/state/runtime/precompiled/modexp.go b/state/runtime/precompiled/modexp.go index c8d3f02edb..ee3661602b 100644 --- a/state/runtime/precompiled/modexp.go +++ b/state/runtime/precompiled/modexp.go @@ -83,7 +83,7 @@ func multComplexity(x *big.Int) *big.Int { return x } -func (m *modExp) gas(input []byte, config *chain.ForksInTime) uint64 { +func (m *modExp) gas(input []byte, _ types.Address, config *chain.ForksInTime) uint64 { var val, tail []byte val, tail = m.p.get(input, 32) diff --git a/state/runtime/precompiled/native_transfer.go b/state/runtime/precompiled/native_transfer.go index 1be4a94618..74d0b0ad2c 100644 --- a/state/runtime/precompiled/native_transfer.go +++ b/state/runtime/precompiled/native_transfer.go @@ -11,7 +11,7 @@ import ( type nativeTransfer struct{} -func (c *nativeTransfer) gas(input []byte, _ *chain.ForksInTime) uint64 { +func (c *nativeTransfer) gas(_ []byte, _ types.Address, _ *chain.ForksInTime) uint64 { return 21000 } diff --git a/state/runtime/precompiled/native_transfer_test.go b/state/runtime/precompiled/native_transfer_test.go index 4bdf95b4fa..f06e286f33 100644 --- a/state/runtime/precompiled/native_transfer_test.go +++ b/state/runtime/precompiled/native_transfer_test.go @@ -59,6 +59,7 @@ type dummyHost struct { t *testing.T balances map[types.Address]*big.Int + context *runtime.TxContext } func newDummyHost(t *testing.T) *dummyHost { @@ -138,6 +139,10 @@ func (d dummyHost) Selfdestruct(addr types.Address, beneficiary types.Address) { } func (d dummyHost) GetTxContext() runtime.TxContext { + if d.context != nil { + return *d.context + } + d.t.Fatalf("GetTxContext is not implemented") return runtime.TxContext{} diff --git a/state/runtime/precompiled/precompiled.go b/state/runtime/precompiled/precompiled.go index f5f8347a27..93626f953b 100644 --- a/state/runtime/precompiled/precompiled.go +++ b/state/runtime/precompiled/precompiled.go @@ -37,7 +37,7 @@ func init() { } type contract interface { - gas(input []byte, config *chain.ForksInTime) uint64 + gas(input []byte, caller types.Address, config *chain.ForksInTime) uint64 run(input []byte, caller types.Address, host runtime.Host) ([]byte, error) } @@ -49,14 +49,14 @@ type Precompiled struct { } // NewPrecompiled creates a new runtime for the precompiled contracts -func NewPrecompiled() *Precompiled { +func NewPrecompiled(validatorSetBackend ValidatorSetPrecompileBackend) *Precompiled { p := &Precompiled{} - p.setupContracts() + p.setupContracts(validatorSetBackend) return p } -func (p *Precompiled) setupContracts() { +func (p *Precompiled) setupContracts(validatorSetBackend ValidatorSetPrecompileBackend) { p.register("1", &ecrecover{p}) p.register("2", &sha256h{}) p.register("3", &ripemd160h{p}) @@ -79,6 +79,11 @@ func (p *Precompiled) setupContracts() { // BLS aggregated signatures verification precompile p.register(contracts.BLSAggSigsVerificationPrecompile.String(), &blsAggSignsVerification{}) + + // ValidatorSet precompile + p.register(contracts.ValidatorSetPrecompile.String(), &validatorSetPrecompile{ + backend: validatorSetBackend, + }) } func (p *Precompiled) register(precompileAddrRaw string, b contract) { @@ -134,7 +139,7 @@ func (p *Precompiled) Name() string { // Run runs an execution func (p *Precompiled) Run(c *runtime.Contract, host runtime.Host, config *chain.ForksInTime) *runtime.ExecutionResult { contract := p.contracts[c.CodeAddress] - gasCost := contract.gas(c.Input, config) + gasCost := contract.gas(c.Input, c.Caller, config) // In the case of not enough gas for precompiled execution we return ErrOutOfGas if c.Gas < gasCost { diff --git a/state/runtime/precompiled/validator_set_precompile.go b/state/runtime/precompiled/validator_set_precompile.go new file mode 100644 index 0000000000..8932f0117f --- /dev/null +++ b/state/runtime/precompiled/validator_set_precompile.go @@ -0,0 +1,165 @@ +package precompiled + +import ( + "bytes" + "encoding/binary" + "errors" + + "github.com/0xPolygon/polygon-edge/chain" + "github.com/0xPolygon/polygon-edge/consensus/polybft/validator" + "github.com/0xPolygon/polygon-edge/state/runtime" + "github.com/0xPolygon/polygon-edge/types" + "github.com/hashicorp/go-hclog" +) + +const ( + addrOffset = 32 - types.AddressLength + defaultMaxHasQuorumAddresses = 100 +) + +var ( + errValidatorSetPrecompileNotEnabled = errors.New("validator set precompile is not enabled") +) + +// ValidatorSetPrecompileBackend is an interface defining the contract for a precompile backend +// responsible for retrieving validators (current account set) for a specific block number +type ValidatorSetPrecompileBackend interface { + GetValidatorsForBlock(blockNumber uint64) (validator.AccountSet, error) + GetMaxValidatorSetSize() (uint64, error) +} + +// validatorSetPrecompile is a concrete implementation of the contract interface. +// The struct implements two functionalities through the `run` method: +// - isValidator(address addr) bool: Returns true if addr is the address of a validator. +// - hasQuorum(address[] addrs) bool: Returns true if the array of validators is sufficient to constitute a quorum +// It encapsulates a backend that provides the functionality to retrieve validators for a specific block number +type validatorSetPrecompile struct { + backend ValidatorSetPrecompileBackend +} + +// gas returns the gas required to execute the pre-compiled contract +func (c *validatorSetPrecompile) gas(_ []byte, _ types.Address, config *chain.ForksInTime) uint64 { + return 240000 +} + +// Run runs the precompiled contract with the given input. +// There are two functions: +// isValidator(address addr) bool +// hasQuorum(address[] addrs) bool +// Input must be ABI encoded: address or (address[]) +// Output could be an error or ABI encoded "bool" value +func (c *validatorSetPrecompile) run(input []byte, caller types.Address, host runtime.Host) ([]byte, error) { + // if its payable tx we need to look for validator in previous block + blockNumber := uint64(host.GetTxContext().Number) + if !host.GetTxContext().NonPayable { + blockNumber-- + } + + // isValidator case + if len(input) == 32 { + validatorSet, err := createValidatorSet(blockNumber, c.backend) // we are calling validators for previous block + if err != nil { + return nil, err + } + + addr := types.BytesToAddress(input[0:32]) + + if validatorSet.Includes(addr) { + return abiBoolTrue, nil + } + + return abiBoolFalse, nil + } + + maxAddressesCount, err := c.backend.GetMaxValidatorSetSize() + if err != nil { + return nil, err + } + + if maxAddressesCount == 0 { + maxAddressesCount = defaultMaxHasQuorumAddresses + } + + addresses, err := abiDecodeAddresses(input, uint32(maxAddressesCount)) + if err != nil { + return nil, err + } + + validatorSet, err := createValidatorSet(blockNumber, c.backend) + if err != nil { + return nil, err + } + + signers := make(map[types.Address]struct{}, len(addresses)) + for _, x := range addresses { + signers[x] = struct{}{} + } + + if validatorSet.HasQuorum(blockNumber, signers) { + return abiBoolTrue, nil + } + + return abiBoolFalse, nil +} + +func createValidatorSet(blockNumber uint64, backend ValidatorSetPrecompileBackend) (validator.ValidatorSet, error) { + if backend == nil { + return nil, errValidatorSetPrecompileNotEnabled + } + + accounts, err := backend.GetValidatorsForBlock(blockNumber) + if err != nil { + return nil, err + } + + return validator.NewValidatorSet(accounts, hclog.NewNullLogger()), nil +} + +func abiDecodeAddresses(input []byte, maxCount uint32) ([]types.Address, error) { + if len(input) < 64 || len(input)%32 != 0 { + return nil, runtime.ErrInvalidInputData + } + + // abi.encode encodes addresses[] with slice of bytes where initial 31 bytes + // are set to 0, and 32nd is 32 + // then goes length of slice (32 bytes) + // then each address is 32 bytes + dummy := [32]byte{} + dummy[31] = 32 + + if !bytes.Equal(dummy[:], input[:32]) { + return nil, runtime.ErrInvalidInputData + } + + size := binary.BigEndian.Uint32(input[60:64]) + if uint32(len(input)) != size*32+64 { + return nil, runtime.ErrInvalidInputData + } + // sanitize the input if it contains too many addresses. + if maxCount != 0 && size > maxCount { + return nil, runtime.ErrInvalidInputData + } + + res := make([]types.Address, size) + for i, offset := 0, 64; offset < len(input); i, offset = i+1, offset+32 { + res[i] = types.Address(input[offset+addrOffset : offset+32]) + } + + return res, nil +} + +func abiEncodeAddresses(addrs []types.Address) []byte { + res := make([]byte, len(addrs)*32+64) + res[31] = 32 + + binary.BigEndian.PutUint32(res[60:64], uint32(len(addrs))) // 60 == 32 + 28 + + offset := 64 + + for _, addr := range addrs { + copy(res[offset+addrOffset:offset+32], addr.Bytes()) + offset += 32 + } + + return res +} diff --git a/state/runtime/precompiled/validator_set_precompile_test.go b/state/runtime/precompiled/validator_set_precompile_test.go new file mode 100644 index 0000000000..fc0062dd49 --- /dev/null +++ b/state/runtime/precompiled/validator_set_precompile_test.go @@ -0,0 +1,186 @@ +package precompiled + +import ( + "errors" + "math/big" + "testing" + + "github.com/0xPolygon/polygon-edge/consensus/polybft/validator" + "github.com/0xPolygon/polygon-edge/helper/common" + "github.com/0xPolygon/polygon-edge/state/runtime" + "github.com/0xPolygon/polygon-edge/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func Test_ValidatorSetPrecompile_gas(t *testing.T) { + assert.Equal(t, uint64(240000), (&validatorSetPrecompile{}).gas(nil, types.Address{}, nil)) +} + +func Test_ValidatorSetPrecompile_run_BackendNotSet(t *testing.T) { + addr := types.StringToAddress("aaff") + host := newDummyHost(t) + host.context = &runtime.TxContext{ + Number: 100, + } + + p := &validatorSetPrecompile{} + _, err := p.run(common.PadLeftOrTrim(addr.Bytes(), 32), types.Address{}, host) + + assert.ErrorIs(t, err, errValidatorSetPrecompileNotEnabled) +} + +func Test_ValidatorSetPrecompile_run_GetValidatorsForBlockError(t *testing.T) { + desiredErr := errors.New("aaabbb") + addr := types.StringToAddress("aaff") + host := newDummyHost(t) + host.context = &runtime.TxContext{ + Number: 100, + NonPayable: true, + } + backendMock := &validatorSetBackendMock{} + + backendMock.On("GetValidatorsForBlock", uint64(host.context.Number)).Return((validator.AccountSet)(nil), desiredErr) + + p := &validatorSetPrecompile{ + backend: backendMock, + } + _, err := p.run(common.PadLeftOrTrim(addr.Bytes(), 32), types.Address{}, host) + + assert.ErrorIs(t, err, desiredErr) +} + +func Test_ValidatorSetPrecompile_run_IsValidator(t *testing.T) { + accounts := getDummyAccountSet() + addrBad := types.StringToAddress("1") + host := newDummyHost(t) + host.context = &runtime.TxContext{ + Number: 100, + NonPayable: false, + } + backendMock := &validatorSetBackendMock{} + + backendMock.On("GetValidatorsForBlock", uint64(host.context.Number-1)).Return(accounts, error(nil)) + + p := &validatorSetPrecompile{ + backend: backendMock, + } + + for _, x := range accounts { + v, err := p.run(common.PadLeftOrTrim(x.Address[:], 32), types.Address{}, host) + require.NoError(t, err) + assert.Equal(t, abiBoolTrue, v) + } + + v, err := p.run(common.PadLeftOrTrim(addrBad.Bytes(), 32), types.Address{}, host) + require.NoError(t, err) + assert.Equal(t, abiBoolFalse, v) +} + +func Test_ValidatorSetPrecompile_run_HasQuorum(t *testing.T) { + accounts := getDummyAccountSet() + addrGood := []types.Address{ + accounts[0].Address, + accounts[1].Address, + accounts[3].Address, + } + addrBad1 := []types.Address{ + accounts[0].Address, + } + addrBad2 := []types.Address{ + accounts[0].Address, + types.StringToAddress("0"), + accounts[3].Address, + } + host := newDummyHost(t) + host.context = &runtime.TxContext{ + Number: 200, + NonPayable: true, + } + backendMock := &validatorSetBackendMock{} + + backendMock.On("GetValidatorsForBlock", uint64(host.context.Number)).Return(accounts, error(nil)) + backendMock.On("GetMaxValidatorSetSize").Return(uint64(100), error(nil)).Twice() + backendMock.On("GetMaxValidatorSetSize").Return(uint64(0), error(nil)).Once() + + p := &validatorSetPrecompile{ + backend: backendMock, + } + + v, err := p.run(abiEncodeAddresses(addrGood), types.Address{}, host) + require.NoError(t, err) + assert.Equal(t, abiBoolTrue, v) + + v, err = p.run(abiEncodeAddresses(addrBad1), types.Address{}, host) + require.NoError(t, err) + assert.Equal(t, abiBoolFalse, v) + + v, err = p.run(abiEncodeAddresses(addrBad2), types.Address{}, host) + require.NoError(t, err) + assert.Equal(t, abiBoolFalse, v) +} + +func Test_abiDecodeAddresses_Error(t *testing.T) { + dummy1 := [31]byte{} + dummy2 := [62]byte{} + dummy3 := [64]byte{} + dummy4 := [96]byte{} + dummy4[31] = 32 + dummy4[63] = 10 + + _, err := abiDecodeAddresses(dummy1[:], 0) + require.ErrorIs(t, err, runtime.ErrInvalidInputData) + + _, err = abiDecodeAddresses(dummy2[:], 0) + require.ErrorIs(t, err, runtime.ErrInvalidInputData) + + _, err = abiDecodeAddresses(dummy3[:], 0) + require.ErrorIs(t, err, runtime.ErrInvalidInputData) + + _, err = abiDecodeAddresses(dummy4[:], 0) + require.ErrorIs(t, err, runtime.ErrInvalidInputData) +} + +type validatorSetBackendMock struct { + mock.Mock +} + +func (m *validatorSetBackendMock) GetValidatorsForBlock(blockNumber uint64) (validator.AccountSet, error) { + call := m.Called(blockNumber) + + return call.Get(0).(validator.AccountSet), call.Error(1) +} + +func (m *validatorSetBackendMock) GetMaxValidatorSetSize() (uint64, error) { + call := m.Called() + + return call.Get(0).(uint64), call.Error(1) +} + +func getDummyAccountSet() validator.AccountSet { + v, _ := new(big.Int).SetString("1000000000000000000000", 10) + + return validator.AccountSet{ + &validator.ValidatorMetadata{ + Address: types.StringToAddress("0xd29f66FEd147B26925DE44Ba468670e921012B6f"), + VotingPower: new(big.Int).Set(v), + IsActive: true, + }, + &validator.ValidatorMetadata{ + Address: types.StringToAddress("0x72aB93bbbc38E90d962dcbf41d973f8C434978e1"), + VotingPower: new(big.Int).Set(v), + IsActive: true, + }, + &validator.ValidatorMetadata{ + Address: types.StringToAddress("0x3b5c82720835d3BAA42eB34E3e4acEE6042A830D"), + VotingPower: new(big.Int).Set(v), + IsActive: true, + }, + &validator.ValidatorMetadata{ + Address: types.StringToAddress("0x87558A1abE10E41a328086474c93449e8A1F8f4e"), + VotingPower: new(big.Int).Set(v), + IsActive: true, + }, + } +}