synapsecns/sanguine

View on GitHub
ethergo/backends/geth/geth.go

Summary

Maintainability
A
2 hrs
Test Coverage
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{}