Skip to content

Commit

Permalink
feat(precompile): add getClientChains function
Browse files Browse the repository at this point in the history
This function can be used by the `ExocoreGateway` to fetch all currently
registered client chains. This list is used to send a `markBootstrapped`
transaction to all the destination chains.

TODO: at this point, the function to mark bootstrap on all chains must
be triggered manually. This call should be automated in the future.
  • Loading branch information
MaxMustermann2 committed Apr 17, 2024
1 parent c057736 commit 6d60aec
Show file tree
Hide file tree
Showing 9 changed files with 379 additions and 0 deletions.
21 changes: 21 additions & 0 deletions precompiles/clientchains/abi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[
{
"name": "getClientChains",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "success",
"type": "bool",
"internalType": "bool"
},
{
"name": "chainIds",
"type": "uint16[]",
"internalType": "uint16[]"
}
],
"stateMutability": "view",
"payable": false
}
]
120 changes: 120 additions & 0 deletions precompiles/clientchains/clientchains.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package clientchains

import (
"bytes"
"embed"
"fmt"

assetskeeper "github.com/ExocoreNetwork/exocore/x/assets/keeper"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper"
"github.com/ethereum/go-ethereum/accounts/abi"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
cmn "github.com/evmos/evmos/v14/precompiles/common"
)

var _ vm.PrecompiledContract = &Precompile{}

// Embed abi json file to the executable binary. Needed when importing as dependency.
//
//go:embed abi.json
var f embed.FS

// Precompile defines the precompiled contract for deposit.
type Precompile struct {
cmn.Precompile
assetsKeeper assetskeeper.Keeper
}

// NewPrecompile creates a new deposit Precompile instance as a
// PrecompiledContract interface.
func NewPrecompile(
assetsKeeper assetskeeper.Keeper,
authzKeeper authzkeeper.Keeper,
) (*Precompile, error) {
abiBz, err := f.ReadFile("abi.json")
if err != nil {
return nil, fmt.Errorf("error loading the client chains ABI %s", err)
}

newAbi, err := abi.JSON(bytes.NewReader(abiBz))
if err != nil {
return nil, fmt.Errorf(cmn.ErrInvalidABI, err)
}

return &Precompile{
Precompile: cmn.Precompile{
ABI: newAbi,
AuthzKeeper: authzKeeper,
KvGasConfig: storetypes.KVGasConfig(),
TransientKVGasConfig: storetypes.TransientGasConfig(),
// should be configurable in the future.
ApprovalExpiration: cmn.DefaultExpirationDuration,
},
assetsKeeper: assetsKeeper,
}, nil
}

// Address defines the address of the client chains precompile contract.
// address: 0x0000000000000000000000000000000000000801
func (p Precompile) Address() common.Address {
return common.HexToAddress("0x0000000000000000000000000000000000000801")
}

// RequiredGas calculates the precompiled contract's base gas rate.
func (p Precompile) RequiredGas(input []byte) uint64 {
methodID := input[:4]

method, err := p.MethodById(methodID)
if err != nil {
// This should never happen since this method is going to fail during Run
return 0
}
return p.Precompile.RequiredGas(input, p.IsTransaction(method.Name))
}

// Run executes the precompiled contract client chain methods defined in the ABI.
func (p Precompile) Run(
evm *vm.EVM, contract *vm.Contract, readOnly bool,
) (bz []byte, err error) {
// if the user calls instead of staticcalls, it is their problem. we don't validate that.
ctx, _, method, initialGas, args, err := p.RunSetup(
evm, contract, readOnly, p.IsTransaction,
)
if err != nil {
return nil, err
}

// This handles any out of gas errors that may occur during the execution of a precompile tx
// or query. It avoids panics and returns the out of gas error so the EVM can continue
// gracefully.
defer cmn.HandleGasError(ctx, contract, initialGas, &err)()

bz, err = p.GetClientChains(ctx, method, args)

Check failure on line 96 in precompiles/clientchains/clientchains.go

View workflow job for this annotation

GitHub Actions / Run golangci-lint

File is not `gofumpt`-ed (gofumpt)
if err != nil {
ctx.Logger().Error(
"call client chains precompile error",
"module", "client chains precompile",
"err", err,
)
return nil, err
}

cost := ctx.GasMeter().GasConsumed() - initialGas

if !contract.UseGas(cost) {
return nil, vm.ErrOutOfGas
}

return bz, nil
}

// IsTransaction checks if the given methodID corresponds to a transaction (true)
// or query (false).
func (Precompile) IsTransaction(string) bool {
// there are no transaction methods in this precompile, only queries.
return false
}
19 changes: 19 additions & 0 deletions precompiles/clientchains/clientchains.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
pragma solidity >=0.8.17;

/// @dev The CLIENT_CHAINS contract's address.
address constant CLIENT_CHAINS_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000801;

/// @dev The CLIENT_CHAINS contract's instance.
IClientChains constant CLIENT_CHAINS_CONTRACT = IClientChains(
CLIENT_CHAINS_PRECOMPILE_ADDRESS
);

/// @author Exocore Team
/// @title Client Chains Precompile Contract
/// @dev The interface through which solidity contracts will interact with ClientChains
/// @custom:address 0x0000000000000000000000000000000000000801
interface IClientChains {
/// @dev Returns the chain indices of the client chains.
function getClientChains() external view returns (bool, uint16[] memory);
}

131 changes: 131 additions & 0 deletions precompiles/clientchains/clientchains_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package clientchains_test

import (
"math/big"

"github.com/ExocoreNetwork/exocore/app"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
evmtypes "github.com/evmos/evmos/v14/x/evm/types"
)

func (s *ClientChainsPrecompileSuite) TestIsTransaction() {
testCases := []struct {
name string
method string
isTx bool
}{
{
"non existant method",
"HelloFakeMethod",
false,
},
{
"actual method",
"getClientChains",
false,
},
}

for _, tc := range testCases {
s.Run(tc.name, func() {
s.Require().Equal(s.precompile.IsTransaction(tc.method), tc.isTx)
})
}
}

func paddingClientChainAddress(input []byte, outputLength int) []byte {
if len(input) < outputLength {
padding := make([]byte, outputLength-len(input))
return append(input, padding...)
}
return input
}

func (s *ClientChainsPrecompileSuite) TestGetClientChains() {
input, err := s.precompile.Pack("getClientChains")
s.Require().NoError(err, "failed to pack input")
output, err := s.precompile.Methods["getClientChains"].Outputs.Pack(true, []uint16{101})
s.Require().NoError(err, "failed to pack output")
testcases := []struct {
name string
malleate func() []byte
readOnly bool
expPass bool
errContains string
returnBytes []byte
}{
{
name: "get client chains",
malleate: func() []byte {
return input
},
readOnly: true,
expPass: true,
returnBytes: output,
},
}
for _, tc := range testcases {
tc := tc
s.Run(tc.name, func() {
s.SetupTest()
baseFee := s.App.FeeMarketKeeper.GetBaseFee(s.Ctx)
contract := vm.NewPrecompile(
vm.AccountRef(s.Address),
s.precompile,
big.NewInt(0),
uint64(1e6),
)
contract.Input = tc.malleate()
contractAddr := contract.Address()
txArgs := evmtypes.EvmTxArgs{
ChainID: s.App.EvmKeeper.ChainID(),
Nonce: 0,
To: &contractAddr,
Amount: nil,
GasLimit: 100000,
GasPrice: app.MainnetMinGasPrices.BigInt(),
GasFeeCap: baseFee,
GasTipCap: big.NewInt(1),
Accesses: &ethtypes.AccessList{},
}
msgEthereumTx := evmtypes.NewTx(&txArgs)
msgEthereumTx.From = s.Address.String()
err := msgEthereumTx.Sign(s.EthSigner, s.Signer)
s.Require().NoError(err, "failed to sign Ethereum message")
proposerAddress := s.Ctx.BlockHeader().ProposerAddress
cfg, err := s.App.EvmKeeper.EVMConfig(
s.Ctx, proposerAddress, s.App.EvmKeeper.ChainID(),
)
s.Require().NoError(err, "failed to instantiate EVM config")
msg, err := msgEthereumTx.AsMessage(s.EthSigner, baseFee)
s.Require().NoError(err, "failed to instantiate Ethereum message")
evm := s.App.EvmKeeper.NewEVM(
s.Ctx, msg, cfg, nil, s.StateDB,
)
params := s.App.EvmKeeper.GetParams(s.Ctx)
activePrecompiles := params.GetActivePrecompilesAddrs()
precompileMap := s.App.EvmKeeper.Precompiles(activePrecompiles...)
err = vm.ValidatePrecompiles(precompileMap, activePrecompiles)
s.Require().NoError(err, "invalid precompiles", activePrecompiles)
evm.WithPrecompiles(precompileMap, activePrecompiles)
bz, err := s.precompile.Run(evm, contract, tc.readOnly)
if tc.expPass {
s.Require().NoError(
err, "expected no error when running the precompile",
)
s.Require().Equal(
tc.returnBytes, bz, "the return doesn't match the expected result",
)
} else {
s.Require().Error(
err, "expected error to be returned when running the precompile",
)
s.Require().Nil(
bz, "expected returned bytes to be nil",
)
s.Require().ErrorContains(err, tc.errContains)
}
})
}
}
32 changes: 32 additions & 0 deletions precompiles/clientchains/setup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package clientchains_test

import (
"testing"

"github.com/ExocoreNetwork/exocore/precompiles/clientchains"
"github.com/ExocoreNetwork/exocore/testutil"

"github.com/stretchr/testify/suite"
)

var s *ClientChainsPrecompileSuite

type ClientChainsPrecompileSuite struct {
testutil.BaseTestSuite

precompile *clientchains.Precompile
}

func TestPrecompileTestSuite(t *testing.T) {
s = new(ClientChainsPrecompileSuite)
suite.Run(t, s)
}

func (s *ClientChainsPrecompileSuite) SetupTest() {
s.DoSetupTest()
precompile, err := clientchains.NewPrecompile(
s.App.AssetsKeeper, s.App.AuthzKeeper,
)
s.Require().NoError(err)
s.precompile = precompile
}
39 changes: 39 additions & 0 deletions precompiles/clientchains/tx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package clientchains

import (
"math"

errorsmod "cosmossdk.io/errors"
assetstypes "github.com/ExocoreNetwork/exocore/x/assets/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/accounts/abi"
)

func (p Precompile) GetClientChains(
ctx sdk.Context,
method *abi.Method,
args []interface{},
) ([]byte, error) {
if len(args) > 0 {
return nil, errorsmod.Wrapf(assetstypes.ErrInvalidInput, "no input is required")
}
infos, err := p.assetsKeeper.GetAllClientChainInfo(ctx)
if err != nil {
return nil, err
}
ids := make([]uint16, 0, len(infos))
for id := range infos {
// technically LZ supports uint32, but unfortunately all the precompiles
// based it on uint16, so we have to stick with it.
// TODO: change it to uint32 here and in other precompiles.
if id > math.MaxUint16 {
return nil, errorsmod.Wrapf(
assetstypes.ErrInvalidInput, "client chain id is too large",
)
}
// #nosec G701 // already checked
convId := uint16(id)

Check warning on line 35 in precompiles/clientchains/tx.go

View workflow job for this annotation

GitHub Actions / Run golangci-lint

var-naming: var convId should be convID (revive)
ids = append(ids, convId)
}

Check warning

Code scanning / CodeQL

Iteration over map Warning

Iteration over map may be a possible source of non-determinism
return method.Outputs.Pack(true, ids)
}
6 changes: 6 additions & 0 deletions x/assets/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ var (
ModuleName, 0,
"there is no stored key for the input chain index",
)

ErrNoClientChainAssetKey = errorsmod.Register(
ModuleName, 1,
"there is no stored key for the input assetID",
Expand Down Expand Up @@ -80,4 +81,9 @@ var (
ModuleName, 14,
"the genesis data supplied is invalid",
)

ErrInvalidInput = errorsmod.Register(
ModuleName, 15,
"the input is invalid",
)
)
Loading

0 comments on commit 6d60aec

Please sign in to comment.