prysm-pulse/testing/endtoend/components/eth1/depositor.go
terence 5a66807989
Update to V5 (#13622)
* First take at updating everything to v5

* Patch gRPC gateway to use prysm v5

Fix patch

* Update go ssz

---------

Co-authored-by: Preston Van Loon <pvanloon@offchainlabs.com>
2024-02-15 05:46:47 +00:00

234 lines
8.7 KiB
Go

package eth1
import (
"context"
"fmt"
"math/big"
"os"
"sync"
"time"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
gethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v5/config/params"
contracts "github.com/prysmaticlabs/prysm/v5/contracts/deposit"
"github.com/prysmaticlabs/prysm/v5/encoding/bytesutil"
eth "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1"
e2e "github.com/prysmaticlabs/prysm/v5/testing/endtoend/params"
"github.com/prysmaticlabs/prysm/v5/testing/endtoend/types"
"github.com/prysmaticlabs/prysm/v5/testing/util"
log "github.com/sirupsen/logrus"
)
var gweiPerEth = big.NewInt(int64(params.BeaconConfig().GweiPerEth))
func amtInGwei(deposit *eth.Deposit) *big.Int {
amt := big.NewInt(0).SetUint64(deposit.Data.Amount)
return amt.Mul(amt, gweiPerEth)
}
// computeDeposits uses the deterministic validator generator to generate deposits for `nvals` (number of validators).
// To control which validators should receive deposits, so that we can generate deposits at different stages of e2e,
// the `offset` parameter skips the first N validators in the deterministic list.
// In order to test the requirement that our deposit follower is able to handle multiple partial deposits,
// the `partial` flag specifies that half of the deposits should be broken up into 2 transactions.
func computeDeposits(offset, nvals int, partial bool) ([]*eth.Deposit, error) {
balances := make([]uint64, offset+nvals)
partialIndex := len(balances) // set beyond loop invariant so by default nothing gets partial
if partial {
// Validators in this range will get 2 half (MAX_EFFECTIVE_BALANCE/2) deposits.
// Upper half of the range of desired validator indices is used because these can be easily
// duplicated with a slice from this offset to the end.
partialIndex = offset + nvals/2
}
// Start setting values at `offset`. Lower elements will be discarded - setting them to zero
// ensures they don't slip through and add extra deposits.
for i := offset; i < len(balances); i++ {
if i >= partialIndex {
// these deposits will be duplicated, resulting in 2 deposits summing to MAX_EFFECTIVE_BALANCE
balances[i] = params.BeaconConfig().MaxEffectiveBalance / 2
} else {
balances[i] = params.BeaconConfig().MaxEffectiveBalance
}
}
deposits, _, err := util.DepositsWithBalance(balances)
if err != nil {
return []*eth.Deposit{}, err
}
// if partial = false, these will be a no-op (partialIndex == len(deposits)),
// otherwise it will duplicate the partial deposits.
deposits = append(deposits[offset:], deposits[partialIndex:]...)
return deposits, nil
}
type Depositor struct {
// The EmptyComponent type is embedded in the Depositor so that it satisfies the ComponentRunner interface.
// This allows other components or e2e set up code to block until its Start method has been called.
types.EmptyComponent
Key *keystore.Key
Client *ethclient.Client
ChainID *big.Int
NetworkId *big.Int
cd *contracts.DepositContract
sent *DepositHistory
}
var _ types.ComponentRunner = &Depositor{}
// History exposes the DepositHistory value for a Depositor.
func (d *Depositor) History() *DepositHistory {
if d.sent == nil {
d.sent = &DepositHistory{}
}
return d.sent
}
// DepositHistory is a type used by Depositor to keep track of batches of deposit for test assertion purposes.
type DepositHistory struct {
sync.RWMutex
byBatch map[types.DepositBatch][]*SentDeposit
deposits []*SentDeposit
}
func (h *DepositHistory) record(sd *SentDeposit) {
h.Lock()
defer h.Unlock()
h.deposits = append(h.deposits, sd)
h.updateByBatch(sd)
}
func (h *DepositHistory) updateByBatch(sd *SentDeposit) {
if h.byBatch == nil {
h.byBatch = make(map[types.DepositBatch][]*SentDeposit)
}
h.byBatch[sd.batch] = append(h.byBatch[sd.batch], sd)
}
// Balances sums, by validator, all deposit amounts that were sent as part of the given batch.
// This can be used in e2e evaluators to check that the results of deposit transactions are visible on chain.
func (h *DepositHistory) Balances(batch types.DepositBatch) map[[48]byte]uint64 {
balances := make(map[[48]byte]uint64)
h.RLock()
defer h.RUnlock()
for _, d := range h.byBatch[batch] {
k := bytesutil.ToBytes48(d.deposit.Data.PublicKey)
if _, ok := balances[k]; !ok {
balances[k] = d.deposit.Data.Amount
} else {
balances[k] += d.deposit.Data.Amount
}
}
return balances
}
// SentDeposit is the record of an individual deposit which has been successfully submitted as a transaction.
type SentDeposit struct {
root [32]byte
deposit *eth.Deposit
tx *gethtypes.Transaction
time time.Time
batch types.DepositBatch
}
// SendAndMine uses the deterministic validator generator to generate deposits for `nvals` (number of validators).
// To control which validators should receive deposits, so that we can generate deposits at different stages of e2e,
// the `offset` parameter skips the first N validators in the deterministic list.
// In order to test the requirement that our deposit follower is able to handle multiple partial deposits,
// the `partial` flag specifies that half of the deposits should be broken up into 2 transactions.
// Once the set of deposits has been generated, it submits a transaction for each deposit
// (using 2 transactions for partial deposits) and then uses WaitForBlocks (which spams the miner node with transactions
// to and from its own address) to advance the chain until it has moved forward ETH1_FOLLOW_DISTANCE blocks.
func (d *Depositor) SendAndMine(ctx context.Context, offset, nvals int, batch types.DepositBatch, partial bool) error {
balance, err := d.Client.BalanceAt(ctx, d.Key.Address, nil)
if err != nil {
return err
}
log.WithField("balance", balance.String()).WithField("account", d.Key.Address.Hex()).Info("SendAndMine balance check")
// This is the "Send" part of the function. Compute deposits for `nvals` validators,
// with half of those deposits being split over 2 transactions if the `partial` flag is true,
// and throwing away any validators before `offset`.
deposits, err := computeDeposits(offset, nvals, partial)
if err != nil {
return err
}
txo, err := d.txops(ctx)
if err != nil {
return err
}
for _, dd := range deposits {
if err := d.SendDeposit(dd, txo, batch); err != nil {
return err
}
}
// This is the "AndMine" part of the function. WaitForBlocks will spam transactions to/from the given key
// to advance the EL chain and until the chain has advanced the requested amount.
if err = WaitForBlocks(d.Client, d.Key, params.BeaconConfig().Eth1FollowDistance); err != nil {
return fmt.Errorf("failed to mine blocks %w", err)
}
return nil
}
// SendDeposit sends a single deposit. A record of this deposit will be tracked for the life of the Depositor,
// allowing evaluators to use the deposit history to make assertions about those deposits.
func (d *Depositor) SendDeposit(dep *eth.Deposit, txo *bind.TransactOpts, batch types.DepositBatch) error {
contract, err := d.contractDepositor()
if err != nil {
return err
}
txo.Value = amtInGwei(dep)
root, err := dep.Data.HashTreeRoot()
if err != nil {
return err
}
sent := time.Now()
tx, err := contract.Deposit(txo, dep.Data.PublicKey, dep.Data.WithdrawalCredentials, dep.Data.Signature, root)
if err != nil {
return errors.Wrap(err, "unable to send transaction to contract")
}
d.History().record(&SentDeposit{deposit: dep, time: sent, tx: tx, root: root, batch: batch})
txo.Nonce = txo.Nonce.Add(txo.Nonce, big.NewInt(1))
return nil
}
// txops is a little helper method that creates a TransactOpts (type encapsulating auth/meta data needed to make a tx).
func (d *Depositor) txops(ctx context.Context) (*bind.TransactOpts, error) {
txo, err := bind.NewKeyedTransactorWithChainID(d.Key.PrivateKey, d.NetworkId)
if err != nil {
return nil, err
}
txo.Context = ctx
txo.GasLimit = e2e.DepositGasLimit
nonce, err := d.Client.PendingNonceAt(ctx, txo.From)
if err != nil {
return nil, err
}
txo.Nonce = big.NewInt(0).SetUint64(nonce)
return txo, nil
}
// contractDepositor is a little helper method that inits and caches a DepositContract value.
// DepositContract is a special-purpose client for calling the deposit contract.
func (d *Depositor) contractDepositor() (*contracts.DepositContract, error) {
if d.cd == nil {
addr := common.HexToAddress(params.BeaconConfig().DepositContractAddress)
contract, err := contracts.NewDepositContract(addr, d.Client)
if err != nil {
return nil, err
}
d.cd = contract
}
return d.cd, nil
}
func (d *Depositor) UnderlyingProcess() *os.Process {
return nil // No subprocess for this component.
}