ethergo/backends/geth/geth.go
package geth
import (
"context"
"github.com/ethereum/go-ethereum/eth/catalyst"
"github.com/synapsecns/sanguine/ethergo/client"
"math/big"
"os"
"testing"
"time"
"github.com/Flaque/filet"
"github.com/brianvoe/gofakeit/v6"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common"
ethCore "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/gasprice"
"github.com/ethereum/go-ethereum/eth/tracers"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/graphql"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
"github.com/lmittmann/w3"
"github.com/lmittmann/w3/w3types"
"github.com/stretchr/testify/assert"
"github.com/synapsecns/sanguine/core"
"github.com/synapsecns/sanguine/ethergo/backends"
"github.com/synapsecns/sanguine/ethergo/backends/base"
"github.com/synapsecns/sanguine/ethergo/chain"
legacyClient "github.com/synapsecns/sanguine/ethergo/chain/client"
"github.com/teivah/onecontext"
"k8s.io/apimachinery/pkg/util/wait"
)
const gasLimit = 10000000
// NewEmbeddedBackendForChainID gets the embedded backend for a specific chain id.
func NewEmbeddedBackendForChainID(ctx context.Context, t *testing.T, chainID *big.Int) *Backend {
t.Helper()
// get the default config
config := ethCore.DeveloperGenesisBlock(gasLimit, &common.Address{}).Config
config.ChainID = chainID
return NewEmbeddedBackendWithConfig(ctx, t, config)
}
// NewEmbeddedBackend gets the default embedded backend.
func NewEmbeddedBackend(ctx context.Context, t *testing.T) *Backend {
t.Helper()
return NewEmbeddedBackendForChainID(ctx, t, params.AllCliqueProtocolChanges.ChainID)
}
// see: https://git.io/JGsC1
// taken from geth, used to speed up tests.
const (
veryLightScryptN = 2
veryLightScryptP = 1
)
// NewEmbeddedBackendWithConfig gets a full node backend to test against and returns the rpc url
// can be canceled with the past in context object.
func NewEmbeddedBackendWithConfig(ctx context.Context, t *testing.T, config *params.ChainConfig) *Backend {
t.Helper()
setupEthLogger()
embedded := Backend{}
logger.Debug("creating eth node")
kstr := keystore.NewKeyStore(filet.TmpDir(t, ""), veryLightScryptN, veryLightScryptP)
password := gofakeit.Password(true, true, true, false, false, 10)
acct, err := kstr.NewAccount(password)
assert.Nil(t, err)
data, err := os.ReadFile(acct.URL.Path)
assert.Nil(t, err)
key, err := keystore.DecryptKey(data, password)
assert.Nil(t, err)
embedded.faucetAddr = key
assert.Nil(t, err)
embedded.Node, err = node.New(makeNodeConfig(t))
assert.Nil(t, err)
ethConfig := makeEthConfig(embedded.faucetAddr.Address, config)
embedded.ethBackend, err = eth.New(embedded.Node, ethConfig)
assert.Nil(t, err)
// setup the consensus client
simBeacon, err := catalyst.NewSimulatedBeacon(1, embedded.ethBackend)
assert.Nil(t, err)
catalyst.RegisterSimulatedBeaconAPIs(embedded.Node, simBeacon)
embedded.Node.RegisterLifecycle(simBeacon)
embedded.Node.RegisterAPIs(toPublic(tracers.APIs(embedded.ethBackend.APIBackend)))
// Configure log filter RPC API.
filterSystem := utils.RegisterFilterAPI(embedded.Node, embedded.ethBackend.APIBackend, ethConfig)
// TODO: this service should be optional. We use it enough in debugging right now to enable globally
err = graphql.New(embedded.Node, embedded.ethBackend.APIBackend, filterSystem, embedded.Node.Config().GraphQLCors, embedded.Node.Config().GraphQLVirtualHosts)
assert.Nil(t, err)
assert.Nil(t, embedded.Node.Start())
// import the faucet account (etherbase)
// add the scrypt test backend
keystoreBackend := keystore.NewKeyStore(filet.TmpDir(t, ""), veryLightScryptN, veryLightScryptP)
file, err := os.ReadFile(acct.URL.Path)
assert.Nil(t, err)
acct, err = keystoreBackend.Import(file, password, password)
assert.Nil(t, err)
assert.Nil(t, keystoreBackend.Unlock(acct, password))
// set the backend
embedded.ethBackend.AccountManager().AddBackend(keystoreBackend)
embedded.ethBackend.SetEtherbase(acct.Address)
embedded.ethBackend.TxPool().SetGasTip(big.NewInt(0))
err = embedded.ethBackend.APIBackend.StartMining()
assert.Nil(t, err)
// add debugger for node stop
go func() {
embedded.Node.Wait()
logger.Debug("eth node stopped")
}()
go func() {
<-ctx.Done()
assert.Nil(t, embedded.Node.Close())
}()
// wait until the simulated node has started mining
isMiningCtx, cancelMiningCtx := context.WithCancel(ctx)
wait.UntilWithContext(isMiningCtx, func(ctx context.Context) {
if embedded.ethBackend.IsMining() {
cancelMiningCtx()
} else {
_ = embedded.ethBackend.APIBackend.StartMining()
}
}, time.Millisecond*50)
baseClient := embedded.makeClient(t)
chn, err := chain.NewFromClient(ctx, &legacyClient.Config{ChainID: int(config.ChainID.Int64()), RPCUrl: []string{embedded.Node.HTTPEndpoint()}}, baseClient)
assert.Nil(t, err)
chn.SetChainConfig(config)
embedded.Backend, err = base.NewBaseBackend(ctx, t, chn)
assert.Nil(t, err)
return &embedded
}
// Backend is a full geth backend equivalent to running geth --dev.
type Backend struct {
// Chain is the creates chain object
*base.Backend
// Node is the eth node
*node.Node
// faucet addr is they key store used for etherbase
faucetAddr *keystore.Key
// ethBackend is the eth backend
ethBackend *eth.Ethereum
}
func (f *Backend) BatchWithContext(ctx context.Context, calls ...w3types.Caller) error {
return f.BatchContext(ctx, calls...)
}
// ChainConfig gets the chain config for the backend.
func (f *Backend) ChainConfig() *params.ChainConfig {
return f.ethBackend.BlockChain().Config()
}
// Signer gets the signer for the chain.
func (f *Backend) Signer() types.Signer {
latestBlock, err := f.BlockByNumber(f.Context(), nil)
assert.Nil(f.T(), err)
return types.MakeSigner(f.ChainConfig(), latestBlock.Number(), latestBlock.Time())
}
// GethBackendName is the name of the geth backend.
const GethBackendName = "GethBackend"
// BackendName gets the backend name.
func (f *Backend) BackendName() string {
return GethBackendName
}
// GetTxContext gets a signed transaction from full backend.
func (f *Backend) GetTxContext(ctx context.Context, address *common.Address) (res backends.AuthType) {
ctx, cancel := onecontext.Merge(ctx, f.Context())
defer cancel()
var acct *keystore.Key
// TODO handle storing accounts to conform to get tx context
if address != nil {
acct = f.GetAccount(*address)
if acct == nil {
f.T().Errorf("could not get account %s", address.String())
return res
}
} else {
acct = f.GetFundedAccount(ctx, big.NewInt(0).Mul(big.NewInt(params.Ether), big.NewInt(10)))
f.Store(acct)
}
auth, err := f.NewKeyedTransactorFromKey(acct.PrivateKey)
assert.Nil(f.T(), err)
latestBlock, err := f.BlockByNumber(ctx, nil)
assert.Nil(f.T(), err)
err = f.Chain.GasSetter().SetGasFee(ctx, auth, latestBlock.NumberU64(), core.CopyBigInt(gasprice.DefaultMaxPrice))
assert.Nil(f.T(), err)
auth.GasLimit = ethCore.DeveloperGenesisBlock(gasLimit, &f.faucetAddr.Address).GasLimit / 2
return backends.AuthType{
TransactOpts: auth,
PrivateKey: acct.PrivateKey,
}
}
// wrappedClient wraps the legacyClient in one that contains a chain config.
type wrappedClient struct {
*ethclient.Client
rpcClient *rpc.Client
w3Client *w3.Client
chainConfig *params.ChainConfig
}
// ChainConfig gets the chain config from the wrapped legacyClient.
func (w wrappedClient) ChainConfig() *params.ChainConfig {
return w.chainConfig
}
// CallContext calls the call context method on the underlying legacyClient.
func (w wrappedClient) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error {
//nolint:wrapcheck
return w.rpcClient.CallContext(ctx, result, method, args...)
}
// BatchCallContext calls the batch call method on the underlying legacyClient.
func (w wrappedClient) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error {
//nolint:wrapcheck
return w.rpcClient.BatchCallContext(ctx, b)
}
func (w wrappedClient) Web3Version(ctx context.Context) (version string, err error) {
if err := w.rpcClient.CallContext(ctx, &version, client.Web3VersionMethod.String()); err != nil {
// nolint: wrapcheck
return "", err
}
return version, nil
}
func (w wrappedClient) BatchContext(ctx context.Context, calls ...w3types.Caller) error {
//nolint:wrapcheck
return w.w3Client.CallCtx(ctx, calls...)
}
// EVMClient gets a legacyClient for the backend.
func (f *Backend) makeClient(tb testing.TB) *wrappedClient {
tb.Helper()
handler, err := f.RPCHandler()
assert.Nil(tb, err)
rpcClient := rpc.DialInProc(handler)
rawClient := ethclient.NewClient(rpcClient)
w3Client := w3.NewClient(rpcClient)
return &wrappedClient{Client: rawClient, chainConfig: f.ChainConfig(), rpcClient: rpcClient, w3Client: w3Client}
}
// getFaucetTxContext gets a signed transaction from the faucet address.
func (f *Backend) getFaucetTxContext(ctx context.Context) *bind.TransactOpts {
ctx, cancel := onecontext.Merge(ctx, f.Context())
defer cancel()
auth, err := f.NewKeyedTransactorFromKey(f.faucetAddr.PrivateKey)
assert.Nil(f.T(), err)
auth.GasPrice, err = f.Client().SuggestGasPrice(ctx)
assert.Nil(f.T(), err)
auth.GasLimit = ethCore.DeveloperGenesisBlock(gasLimit, &f.faucetAddr.Address).GasLimit / 2
return auth
}
// FaucetSignTx will sign a tx with the faucet addr.
func (f *Backend) FaucetSignTx(tx *types.Transaction) *types.Transaction {
tx, err := f.Backend.SignTx(tx, f.Signer(), f.faucetAddr.PrivateKey)
assert.Nil(f.T(), err)
return tx
}
// FundAccount fundsa new account.
func (f *Backend) FundAccount(ctx context.Context, address common.Address, amount big.Int) {
rawTx := f.getFaucetTxContext(ctx)
tx := f.FaucetSignTx(types.NewTx(&types.LegacyTx{
To: &address,
Value: &amount,
Gas: rawTx.GasLimit,
GasPrice: rawTx.GasPrice,
}))
assert.Nil(f.T(), f.Client().SendTransaction(f.Context(), tx))
// wait for tx confirmation
f.WaitForConfirmation(ctx, tx)
// wait for value of account to be > 0
for {
select {
case <-ctx.Done():
return
default:
// attempt to get around insufficient funds for gas * price + value
newBalance, err := f.Client().BalanceAt(ctx, address, nil)
if err != nil {
continue
}
if newBalance.Cmp(big.NewInt(0)) != 0 {
return
}
}
}
}
// GetFundedAccount returns an account with the requested balance. (Note: if genesis acount has an insufficient
// balance, blocks may be mined here).
func (f *Backend) GetFundedAccount(ctx context.Context, requestBalance *big.Int) *keystore.Key {
key := f.MockAccount()
f.Store(key)
f.FundAccount(ctx, key.Address, *requestBalance)
return key
}
// toPublic converts the analytics to public apis.
func toPublic(apis []rpc.API) (publicApis []rpc.API) {
for _, api := range apis {
api.Public = true
publicApis = append(publicApis, api)
}
return publicApis
}
var _ backends.SimulatedTestBackend = &Backend{}