/
depositor.go
233 lines (211 loc) · 8.69 KB
/
depositor.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
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.
}