diff --git a/rocketpool/api/minipool/status.go b/rocketpool/api/minipool/status.go index 6eedc504f..3f490a866 100644 --- a/rocketpool/api/minipool/status.go +++ b/rocketpool/api/minipool/status.go @@ -3,6 +3,7 @@ package minipool import ( "fmt" + "github.com/ethereum/go-ethereum/common" "github.com/urfave/cli/v3" "github.com/rocket-pool/smartnode/rocketpool/api/response" @@ -48,7 +49,9 @@ func getStatus(c *cli.Command) (*api.MinipoolStatusResponse, error) { if err != nil { return nil, err } - details, err := GetNodeMinipoolDetails(rp, bc, nodeAccount.Address, &legacyMinipoolQueueAddress) + multicallerAddress := common.HexToAddress(cfg.Smartnode.GetMulticallAddress()) + balanceBatcherAddress := common.HexToAddress(cfg.Smartnode.GetBalanceBatcherAddress()) + details, err := GetNodeMinipoolDetails(rp, bc, nodeAccount.Address, &legacyMinipoolQueueAddress, multicallerAddress, balanceBatcherAddress) if err != nil { return nil, err } diff --git a/rocketpool/api/minipool/utils.go b/rocketpool/api/minipool/utils.go index ef7951fd3..8036adffa 100644 --- a/rocketpool/api/minipool/utils.go +++ b/rocketpool/api/minipool/utils.go @@ -17,6 +17,8 @@ import ( "github.com/rocket-pool/smartnode/bindings/settings/trustednode" "github.com/rocket-pool/smartnode/bindings/tokens" "github.com/rocket-pool/smartnode/bindings/types" + "github.com/rocket-pool/smartnode/bindings/utils/multicall" + rpstate "github.com/rocket-pool/smartnode/bindings/utils/state" "github.com/rocket-pool/smartnode/shared/math" "github.com/rocket-pool/smartnode/shared/services/beacon" @@ -26,6 +28,7 @@ import ( // Settings const MinipoolDetailsBatchSize = 10 const MinipoolPubkeyBatchSize = 50 +const MinipoolQueuePositionBatchSize = 700 // Validate that a minipool belongs to a node func validateMinipoolOwner(mp minipool.Minipool, nodeAddress common.Address) error { @@ -39,31 +42,69 @@ func validateMinipoolOwner(mp minipool.Minipool, nodeAddress common.Address) err return nil } -// Get all node minipool details -func GetNodeMinipoolDetails(rp *rocketpool.RocketPool, bc beacon.Client, nodeAddress common.Address, legacyMinipoolQueueAddress *common.Address) ([]api.MinipoolDetails, error) { +// Get all node minipool details, batched via multicall +func GetNodeMinipoolDetails(rp *rocketpool.RocketPool, bc beacon.Client, nodeAddress common.Address, legacyMinipoolQueueAddress *common.Address, multicallerAddress common.Address, balanceBatcherAddress common.Address) ([]api.MinipoolDetails, error) { + + // Pin every multicall batch below to the same EL block + latestBlockNumber, err := rp.Client.BlockNumber(context.Background()) + if err != nil { + return nil, fmt.Errorf("Error getting latest block number: %w", err) + } + opts := &bind.CallOpts{BlockNumber: big.NewInt(0).SetUint64(latestBlockNumber)} + + // Build the minimal set of contracts the batched fetch needs + mc, err := multicall.NewMultiCaller(rp.Client, multicallerAddress) + if err != nil { + return nil, fmt.Errorf("Error creating multicaller: %w", err) + } + bb, err := multicall.NewBalanceBatcher(rp.Client, balanceBatcherAddress) + if err != nil { + return nil, fmt.Errorf("Error creating balance batcher: %w", err) + } + minipoolManager, err := rp.GetContract("rocketMinipoolManager", nil) + if err != nil { + return nil, fmt.Errorf("Error getting the minipool manager contract: %w", err) + } + minipoolBondReducer, err := rp.GetContract("rocketMinipoolBondReducer", nil) + if err != nil { + return nil, fmt.Errorf("Error getting the minipool bond reducer contract: %w", err) + } + minipoolQueue, err := rp.GetContract("rocketMinipoolQueue", nil) + if err != nil { + return nil, fmt.Errorf("Error getting the minipool queue contract: %w", err) + } + rethContract, err := rp.GetContract("rocketTokenRETH", nil) + if err != nil { + return nil, fmt.Errorf("Error getting the rETH token contract: %w", err) + } + rplContract, err := rp.GetContract("rocketTokenRPL", nil) + if err != nil { + return nil, fmt.Errorf("Error getting the RPL token contract: %w", err) + } + fixedSupplyRplContract, err := rp.GetContract("rocketTokenRPLFixedSupply", nil) + if err != nil { + return nil, fmt.Errorf("Error getting the fixed-supply RPL token contract: %w", err) + } + contracts := &rpstate.NetworkContracts{ + Multicaller: mc, + BalanceBatcher: bb, + RocketMinipoolManager: minipoolManager, + RocketMinipoolBondReducer: minipoolBondReducer, + RocketStorage: rp.RocketStorageContract, + ElBlockNumber: opts.BlockNumber, + } // Data var wg1 errgroup.Group - var addresses []common.Address - var eth2Config beacon.Eth2Config + var nativeMinipoolDetails []rpstate.NativeMinipoolDetails var currentEpoch uint64 - var currentBlock uint64 - - // Get minipool addresses - wg1.Go(func() error { - var err error - addresses, err = minipool.GetNodeMinipoolAddresses(rp, nodeAddress, nil) - return err - }) - // Get eth2 config wg1.Go(func() error { var err error - eth2Config, err = bc.GetEth2Config() + nativeMinipoolDetails, err = rpstate.GetNodeNativeMinipoolDetails(rp, contracts, nodeAddress) return err }) - // Get current epoch wg1.Go(func() error { head, err := bc.GetBeaconHead() if err == nil { @@ -72,76 +113,88 @@ func GetNodeMinipoolDetails(rp *rocketpool.RocketPool, bc beacon.Client, nodeAdd return err }) - // Get current block - wg1.Go(func() error { - header, err := rp.Client.HeaderByNumber(context.Background(), nil) - if err == nil { - currentBlock = header.Number.Uint64() - } - return err - }) - // Wait for data if err := wg1.Wait(); err != nil { return []api.MinipoolDetails{}, err } - // Get minipool validator statuses - validators, err := GetMinipoolValidators(rp, bc, addresses, nil, nil) - if err != nil { - return []api.MinipoolDetails{}, err + addresses := make([]common.Address, len(nativeMinipoolDetails)) + pubkeys := make([]types.ValidatorPubkey, len(nativeMinipoolDetails)) + for i, nmd := range nativeMinipoolDetails { + addresses[i] = nmd.MinipoolAddress + pubkeys[i] = nmd.Pubkey } - // Load details in batches - details := make([]api.MinipoolDetails, len(addresses)) - for bsi := 0; bsi < len(addresses); bsi += MinipoolDetailsBatchSize { + // Get Beacon validator statuses and the EL-side queue positions/token balances + var wg2 errgroup.Group + var validators map[common.Address]beacon.ValidatorStatus + var queuePositions []int64 + var rethBalances, rplBalances, fixedSupplyRplBalances []*big.Int - // Get batch start & end index - msi := bsi - mei := bsi + MinipoolDetailsBatchSize - if mei > len(addresses) { - mei = len(addresses) - } + wg2.Go(func() error { + var err error + validators, err = buildMinipoolValidatorMap(addresses, pubkeys, bc, nil) + return err + }) - // Load details - var wg errgroup.Group - for mi := msi; mi < mei; mi++ { - mi := mi - wg.Go(func() error { - address := addresses[mi] - validator := validators[address] - mpDetails, err := getMinipoolDetails(rp, address, validator, eth2Config, currentEpoch, currentBlock, legacyMinipoolQueueAddress) - if err == nil { - details[mi] = mpDetails - } - return err - }) + wg2.Go(func() error { + var err error + queuePositions, err = getMinipoolQueuePositions(minipoolQueue, mc, addresses, opts) + if err != nil { + return err } - if err := wg.Wait(); err != nil { - return []api.MinipoolDetails{}, err + rethBalances, rplBalances, fixedSupplyRplBalances, err = getMinipoolTokenBalances(mc, rethContract, rplContract, fixedSupplyRplContract, addresses, opts) + return err + }) + + if err := wg2.Wait(); err != nil { + return []api.MinipoolDetails{}, err + } + + // Compute the Beacon-chain share of balance for staking minipools with an activated validator + beaconBalances := make([]*big.Int, len(nativeMinipoolDetails)) + minipoolPtrs := make([]*rpstate.NativeMinipoolDetails, len(nativeMinipoolDetails)) + for i := range nativeMinipoolDetails { + nmd := &nativeMinipoolDetails[i] + minipoolPtrs[i] = nmd + beaconBalances[i] = big.NewInt(0) + if nmd.Status == types.Staking || (nmd.Status == types.Dissolved && !nmd.Finalised) { + validator := validators[nmd.MinipoolAddress] + if validator.Exists && validator.ActivationEpoch < currentEpoch { + beaconBalances[i] = math.GweiToWei(float64(validator.Balance)) + } } + } + if err := rpstate.CalculateCompleteMinipoolShares(rp, contracts, minipoolPtrs, beaconBalances); err != nil { + return []api.MinipoolDetails{}, fmt.Errorf("error calculating minipool shares: %w", err) + } + // Build the response + details := make([]api.MinipoolDetails, len(nativeMinipoolDetails)) + for i := range nativeMinipoolDetails { + nmd := &nativeMinipoolDetails[i] + details[i] = buildMinipoolDetails(nmd, validators[nmd.MinipoolAddress], queuePositions[i], currentEpoch, rethBalances[i], rplBalances[i], fixedSupplyRplBalances[i]) } // Get the scrub period - scrubPeriodSeconds, err := trustednode.GetScrubPeriod(rp, nil) + scrubPeriodSeconds, err := trustednode.GetScrubPeriod(rp, opts) if err != nil { return nil, err } scrubPeriod := time.Duration(scrubPeriodSeconds) * time.Second // Get the dissolve timeout - timeout, err := protocol.GetMinipoolLaunchTimeout(rp, nil) + timeout, err := protocol.GetMinipoolLaunchTimeout(rp, opts) if err != nil { return nil, err } - // Get the time of the latest block - latestEth1Block, err := rp.Client.HeaderByNumber(context.Background(), nil) + // Get the time of the pinned block + pinnedEth1Block, err := rp.Client.HeaderByNumber(context.Background(), opts.BlockNumber) if err != nil { return nil, fmt.Errorf("Can't get the latest block time: %w", err) } - latestBlockTime := time.Unix(int64(latestEth1Block.Time), 0) + latestBlockTime := time.Unix(int64(pinnedEth1Block.Time), 0) // Check the stake status of each minipool for i, mpDetails := range details { @@ -158,7 +211,7 @@ func GetNodeMinipoolDetails(rp *rocketpool.RocketPool, bc beacon.Client, nodeAdd } // Get the promotion scrub period - promotionScrubPeriodSeconds, err := trustednode.GetPromotionScrubPeriod(rp, nil) + promotionScrubPeriodSeconds, err := trustednode.GetPromotionScrubPeriod(rp, opts) if err != nil { return nil, err } @@ -182,134 +235,65 @@ func GetNodeMinipoolDetails(rp *rocketpool.RocketPool, bc beacon.Client, nodeAdd } -// Get a minipool's details -func getMinipoolDetails(rp *rocketpool.RocketPool, minipoolAddress common.Address, validator beacon.ValidatorStatus, eth2Config beacon.Eth2Config, currentEpoch, currentBlock uint64, legacyMinipoolQueueAddress *common.Address) (api.MinipoolDetails, error) { - - // Create minipool - mp, err := minipool.NewMinipool(rp, minipoolAddress, nil) - if err != nil { - return api.MinipoolDetails{}, err - } - - // Data - var wg errgroup.Group - details := api.MinipoolDetails{Address: minipoolAddress} - - // Load data - wg.Go(func() error { - var err error - details.ValidatorPubkey, err = minipool.GetMinipoolPubkey(rp, minipoolAddress, nil) - return err - }) - wg.Go(func() error { - var err error - details.Status, err = mp.GetStatusDetails(nil) - return err - }) - wg.Go(func() error { - var err error - details.DepositType, err = minipool.GetMinipoolDepositType(rp, minipoolAddress, nil) - return err - }) - wg.Go(func() error { - var err error - details.Node, err = mp.GetNodeDetails(nil) - return err - }) - wg.Go(func() error { - var err error - details.User, err = mp.GetUserDetails(nil) - return err - }) - wg.Go(func() error { - var err error - details.Balances, err = tokens.GetBalances(rp, minipoolAddress, nil) - if err != nil { - return fmt.Errorf("error getting minipool %s balances: %w", minipoolAddress.Hex(), err) - } - return err - }) - wg.Go(func() error { - var err error - details.UseLatestDelegate, err = mp.GetUseLatestDelegate(nil) - return err - }) - wg.Go(func() error { - var err error - details.Delegate, err = mp.GetDelegate(nil) - return err - }) - wg.Go(func() error { - var err error - details.PreviousDelegate, err = mp.GetPreviousDelegate(nil) - return err - }) - wg.Go(func() error { - var err error - details.EffectiveDelegate, err = mp.GetEffectiveDelegate(nil) - return err - }) - wg.Go(func() error { - var err error - details.Finalised, err = mp.GetFinalised(nil) - return err - }) - wg.Go(func() error { - var err error - details.Penalties, err = minipool.GetMinipoolPenaltyCount(rp, minipoolAddress, nil) - return err - }) - wg.Go(func() error { - var err error - details.Queue, err = minipool.GetQueueDetails(rp, mp.GetAddress(), nil) - return err - }) - - // Wait for data - if err := wg.Wait(); err != nil { - return api.MinipoolDetails{}, err - } - - // Get node share of balance - if details.Balances.ETH.Cmp(details.Node.RefundBalance) == -1 { - details.NodeShareOfETHBalance = big.NewInt(0) - } else { - effectiveBalance := big.NewInt(0).Sub(details.Balances.ETH, details.Node.RefundBalance) - details.NodeShareOfETHBalance, err = mp.CalculateNodeShare(effectiveBalance, nil) - if err != nil { - return api.MinipoolDetails{}, fmt.Errorf("error calculating node share: %w", err) - } - } - - // Get validator details if staking - if details.Status.Status == types.Staking || (details.Status.Status == types.Dissolved && !details.Finalised) { - validatorDetails, err := getMinipoolValidatorDetails(rp, details, validator, eth2Config, currentEpoch) - if err != nil { - return api.MinipoolDetails{}, err - } - details.Validator = validatorDetails +// Build an api.MinipoolDetails from a multicall-batched NativeMinipoolDetails record, its Beacon +// validator status, and its deposit-queue position +func buildMinipoolDetails(nmd *rpstate.NativeMinipoolDetails, validator beacon.ValidatorStatus, queuePosition int64, currentEpoch uint64, rethBalance *big.Int, rplBalance *big.Int, fixedSupplyRplBalance *big.Int) api.MinipoolDetails { + + details := api.MinipoolDetails{ + Address: nmd.MinipoolAddress, + ValidatorPubkey: nmd.Pubkey, + DepositType: nmd.DepositType, + Finalised: nmd.Finalised, + UseLatestDelegate: nmd.UseLatestDelegate, + Delegate: nmd.Delegate, + PreviousDelegate: nmd.PreviousDelegate, + EffectiveDelegate: nmd.EffectiveDelegate, + Penalties: nmd.PenaltyCount.Uint64(), + Queue: minipool.QueueDetails{Position: queuePosition}, + Status: minipool.StatusDetails{ + Status: nmd.Status, + StatusBlock: nmd.StatusBlock.Uint64(), + StatusTime: time.Unix(nmd.StatusTime.Int64(), 0), + IsVacant: nmd.IsVacant, + }, + Node: minipool.NodeDetails{ + Address: nmd.NodeAddress, + Fee: math.WeiToEth(nmd.NodeFee), + DepositBalance: nmd.NodeDepositBalance, + RefundBalance: nmd.NodeRefundBalance, + DepositAssigned: nmd.NodeDepositAssigned, + }, + User: minipool.UserDetails{ + DepositBalance: nmd.UserDepositBalance, + DepositAssigned: nmd.UserDepositAssigned, + DepositAssignedTime: time.Unix(nmd.UserDepositAssignedTime.Int64(), 0), + }, + Balances: tokens.Balances{ + ETH: nmd.Balance, + RETH: rethBalance, + RPL: rplBalance, + FixedSupplyRPL: fixedSupplyRplBalance, + }, + NodeShareOfETHBalance: nmd.NodeShareOfBalance, } - // Update & return details.RefundAvailable = (details.Node.RefundBalance.Cmp(big.NewInt(0)) > 0) && (details.Balances.ETH.Cmp(details.Node.RefundBalance) >= 0) details.CloseAvailable = (details.Status.Status == types.Dissolved) if details.Status.Status == types.Withdrawable { details.WithdrawalAvailable = true } - return details, nil + // Get validator details if staking + if details.Status.Status == types.Staking || (details.Status.Status == types.Dissolved && !details.Finalised) { + details.Validator = buildMinipoolValidatorDetails(nmd, validator, currentEpoch) + } + + return details } -// Get a minipool's validator details -func getMinipoolValidatorDetails(rp *rocketpool.RocketPool, minipoolDetails api.MinipoolDetails, validator beacon.ValidatorStatus, eth2Config beacon.Eth2Config, currentEpoch uint64) (api.ValidatorDetails, error) { +// Build a minipool's validator details from its NativeMinipoolDetails and Beacon status +func buildMinipoolValidatorDetails(nmd *rpstate.NativeMinipoolDetails, validator beacon.ValidatorStatus, currentEpoch uint64) api.ValidatorDetails { - // Create minipool - mp, err := minipool.NewMinipool(rp, minipoolDetails.Address, nil) - if err != nil { - return api.ValidatorDetails{}, err - } - - // Validator details details := api.ValidatorDetails{} // Set validator status details @@ -324,57 +308,81 @@ func getMinipoolValidatorDetails(rp *rocketpool.RocketPool, minipoolDetails api. // use deposit balances if validator not activated if !validatorActivated { details.Balance = new(big.Int) - details.Balance.Add(minipoolDetails.Node.DepositBalance, minipoolDetails.User.DepositBalance) + details.Balance.Add(nmd.NodeDepositBalance, nmd.UserDepositBalance) details.NodeBalance = new(big.Int) - details.NodeBalance.Set(minipoolDetails.Node.DepositBalance) - return details, nil + details.NodeBalance.Set(nmd.NodeDepositBalance) + return details } // Set validator balance details.Balance = math.GweiToWei(float64(validator.Balance)) - // Get expected node balance - blockBalance := math.GweiToWei(float64(validator.Balance)) - nodeBalance, err := mp.CalculateNodeShare(blockBalance, nil) - if err != nil { - return api.ValidatorDetails{}, err - } - details.NodeBalance = nodeBalance + // Node's share of the Beacon balance, already computed via multicall (CalculateCompleteMinipoolShares) + details.NodeBalance = nmd.NodeShareOfBeaconBalance // Return - return details, nil + return details } -func GetMinipoolValidators(rp *rocketpool.RocketPool, bc beacon.Client, addresses []common.Address, callOpts *bind.CallOpts, validatorStatusOpts *beacon.ValidatorStatusOptions) (map[common.Address]beacon.ValidatorStatus, error) { +// Get every minipool's position in the deposit queue +func getMinipoolQueuePositions(minipoolQueue *rocketpool.Contract, mc *multicall.MultiCaller, addresses []common.Address, opts *bind.CallOpts) ([]int64, error) { - // Load minipool validator pubkeys in batches - pubkeys := make([]types.ValidatorPubkey, len(addresses)) - for bsi := 0; bsi < len(addresses); bsi += MinipoolPubkeyBatchSize { - - // Get batch start & end index + positions := make([]int64, len(addresses)) + for bsi := 0; bsi < len(addresses); bsi += MinipoolQueuePositionBatchSize { msi := bsi - mei := min(bsi+MinipoolPubkeyBatchSize, len(addresses)) - - // Load details - var wg errgroup.Group - for mi := msi; mi < mei; mi++ { - mi := mi - wg.Go(func() error { - address := addresses[mi] - pubkey, err := minipool.GetMinipoolPubkey(rp, address, callOpts) - if err == nil { - pubkeys[mi] = pubkey - } - return err - }) + mei := min(bsi+MinipoolQueuePositionBatchSize, len(addresses)) + rawPositions := make([]*big.Int, mei-msi) + for i := msi; i < mei; i++ { + if err := mc.AddCall(minipoolQueue, &rawPositions[i-msi], "getMinipoolPosition", addresses[i]); err != nil { + return nil, fmt.Errorf("error adding queue position call for minipool %s: %w", addresses[i].Hex(), err) + } } - if err := wg.Wait(); err != nil { - return map[common.Address]beacon.ValidatorStatus{}, err + if _, err := mc.FlexibleCall(true, opts); err != nil { + return nil, fmt.Errorf("error executing multicall: %w", err) } + for i := msi; i < mei; i++ { + positions[i] = rawPositions[i-msi].Int64() + 1 + } + } + return positions, nil + +} + +// Get every minipool's rETH/RPL/fixed-supply-RPL token balances +func getMinipoolTokenBalances(mc *multicall.MultiCaller, rethContract *rocketpool.Contract, rplContract *rocketpool.Contract, fixedSupplyRplContract *rocketpool.Contract, addresses []common.Address, opts *bind.CallOpts) ([]*big.Int, []*big.Int, []*big.Int, error) { + rethBalances := make([]*big.Int, len(addresses)) + rplBalances := make([]*big.Int, len(addresses)) + fixedSupplyRplBalances := make([]*big.Int, len(addresses)) + + for bsi := 0; bsi < len(addresses); bsi += MinipoolQueuePositionBatchSize { + msi := bsi + mei := min(bsi+MinipoolQueuePositionBatchSize, len(addresses)) + for i := msi; i < mei; i++ { + if err := mc.AddCall(rethContract, &rethBalances[i], "balanceOf", addresses[i]); err != nil { + return nil, nil, nil, fmt.Errorf("error adding rETH balance call for minipool %s: %w", addresses[i].Hex(), err) + } + if err := mc.AddCall(rplContract, &rplBalances[i], "balanceOf", addresses[i]); err != nil { + return nil, nil, nil, fmt.Errorf("error adding RPL balance call for minipool %s: %w", addresses[i].Hex(), err) + } + if err := mc.AddCall(fixedSupplyRplContract, &fixedSupplyRplBalances[i], "balanceOf", addresses[i]); err != nil { + return nil, nil, nil, fmt.Errorf("error adding fixed-supply RPL balance call for minipool %s: %w", addresses[i].Hex(), err) + } + } + if _, err := mc.FlexibleCall(true, opts); err != nil { + return nil, nil, nil, fmt.Errorf("error executing multicall: %w", err) + } } + return rethBalances, rplBalances, fixedSupplyRplBalances, nil + +} + +// Build a map of minipool address -> Beacon validator status from a matching list of pubkeys +// (deduplicating/filtering null pubkeys before the batched Beacon call) +func buildMinipoolValidatorMap(addresses []common.Address, pubkeys []types.ValidatorPubkey, bc beacon.Client, validatorStatusOpts *beacon.ValidatorStatusOptions) (map[common.Address]beacon.ValidatorStatus, error) { + // Filter out null and duplicate pubkeys filteredPubkeys := []types.ValidatorPubkey{} for _, pubkey := range pubkeys { @@ -416,3 +424,37 @@ func GetMinipoolValidators(rp *rocketpool.RocketPool, bc beacon.Client, addresse return validators, nil } + +func GetMinipoolValidators(rp *rocketpool.RocketPool, bc beacon.Client, addresses []common.Address, callOpts *bind.CallOpts, validatorStatusOpts *beacon.ValidatorStatusOptions) (map[common.Address]beacon.ValidatorStatus, error) { + + // Load minipool validator pubkeys in batches + pubkeys := make([]types.ValidatorPubkey, len(addresses)) + for bsi := 0; bsi < len(addresses); bsi += MinipoolPubkeyBatchSize { + + // Get batch start & end index + msi := bsi + mei := min(bsi+MinipoolPubkeyBatchSize, len(addresses)) + + // Load details + var wg errgroup.Group + for mi := msi; mi < mei; mi++ { + mi := mi + wg.Go(func() error { + address := addresses[mi] + pubkey, err := minipool.GetMinipoolPubkey(rp, address, callOpts) + if err == nil { + pubkeys[mi] = pubkey + } + return err + }) + } + if err := wg.Wait(); err != nil { + return map[common.Address]beacon.ValidatorStatus{}, err + } + + } + + // Return + return buildMinipoolValidatorMap(addresses, pubkeys, bc, validatorStatusOpts) + +} diff --git a/rocketpool/api/node/status.go b/rocketpool/api/node/status.go index 04473ebea..982180e12 100644 --- a/rocketpool/api/node/status.go +++ b/rocketpool/api/node/status.go @@ -14,10 +14,8 @@ import ( "github.com/rocket-pool/smartnode/bindings/dao/trustednode" "github.com/rocket-pool/smartnode/bindings/megapool" - "github.com/rocket-pool/smartnode/bindings/minipool" "github.com/rocket-pool/smartnode/bindings/network" "github.com/rocket-pool/smartnode/bindings/node" - "github.com/rocket-pool/smartnode/bindings/rocketpool" "github.com/rocket-pool/smartnode/bindings/settings/protocol" "github.com/rocket-pool/smartnode/bindings/tokens" "github.com/rocket-pool/smartnode/bindings/types" @@ -78,6 +76,10 @@ func getStatus(c *cli.Command) (*api.NodeStatusResponse, error) { // Get the legacy MinipoolQueue contract address legacyMinipoolQueueAddress := cfg.Smartnode.GetV110MinipoolQueueAddress() + // Get the multicall / balance batcher addresses used for the batched minipool details fetch + multicallerAddress := common.HexToAddress(cfg.Smartnode.GetMulticallAddress()) + balanceBatcherAddress := common.HexToAddress(cfg.Smartnode.GetBalanceBatcherAddress()) + // Get node account nodeAccount, err := w.GetNodeAccount() if err != nil { @@ -136,7 +138,7 @@ func getStatus(c *cli.Command) (*api.NodeStatusResponse, error) { }) wg.Go(func() error { - mpDetails, err := mp.GetNodeMinipoolDetails(rp, bc, nodeAccount.Address, &legacyMinipoolQueueAddress) + mpDetails, err := mp.GetNodeMinipoolDetails(rp, bc, nodeAccount.Address, &legacyMinipoolQueueAddress, multicallerAddress, balanceBatcherAddress) if err == nil { response.Minipools = mpDetails } @@ -320,45 +322,6 @@ func getStatus(c *cli.Command) (*api.NodeStatusResponse, error) { }) } - // Get node minipool counts - wg.Go(func() error { - details, err := getNodeMinipoolCountDetails(rp, nodeAccount.Address) - if err == nil { - response.MinipoolCounts.Total = len(details) - for _, mpDetails := range details { - if mpDetails.Penalties > 0 { - response.PenalizedMinipools[mpDetails.Address] = mpDetails.Penalties - } - if mpDetails.Finalised { - response.MinipoolCounts.Finalised++ - } else { - switch mpDetails.Status { - case types.Initialized: - response.MinipoolCounts.Initialized++ - case types.Prelaunch: - response.MinipoolCounts.Prelaunch++ - case types.Staking: - response.MinipoolCounts.Staking++ - case types.Withdrawable: - response.MinipoolCounts.Withdrawable++ - case types.Dissolved: - response.MinipoolCounts.Dissolved++ - } - if mpDetails.RefundAvailable { - response.MinipoolCounts.RefundAvailable++ - } - if mpDetails.WithdrawalAvailable { - response.MinipoolCounts.WithdrawalAvailable++ - } - if mpDetails.CloseAvailable { - response.MinipoolCounts.CloseAvailable++ - } - } - } - } - return err - }) - wg.Go(func() error { var err error response.IsFeeDistributorInitialized, err = node.GetFeeDistributorInitialized(rp, nodeAccount.Address, nil) @@ -381,6 +344,39 @@ func getStatus(c *cli.Command) (*api.NodeStatusResponse, error) { return nil, err } + // Derive the minipool counts + response.MinipoolCounts.Total = len(response.Minipools) + for _, mpDetails := range response.Minipools { + if mpDetails.Penalties > 0 { + response.PenalizedMinipools[mpDetails.Address] = mpDetails.Penalties + } + if mpDetails.Finalised { + response.MinipoolCounts.Finalised++ + } else { + switch mpDetails.Status.Status { + case types.Initialized: + response.MinipoolCounts.Initialized++ + case types.Prelaunch: + response.MinipoolCounts.Prelaunch++ + case types.Staking: + response.MinipoolCounts.Staking++ + case types.Withdrawable: + response.MinipoolCounts.Withdrawable++ + case types.Dissolved: + response.MinipoolCounts.Dissolved++ + } + if mpDetails.RefundAvailable { + response.MinipoolCounts.RefundAvailable++ + } + if mpDetails.WithdrawalAvailable { + response.MinipoolCounts.WithdrawalAvailable++ + } + if mpDetails.CloseAvailable { + response.MinipoolCounts.CloseAvailable++ + } + } + } + // Get withdrawal address balances if !bytes.Equal(nodeAccount.Address.Bytes(), response.PrimaryWithdrawalAddress.Bytes()) { withdrawalBalances, err := tokens.GetBalances(rp, response.PrimaryWithdrawalAddress, nil) @@ -443,7 +439,7 @@ func getStatus(c *cli.Command) (*api.NodeStatusResponse, error) { response.BorrowedCollateralRatio = math.WeiToEth(rplPrice) * math.WeiToEth(response.TotalRplStake) / (math.WeiToEth(response.EthBorrowed) + math.WeiToEth(response.PendingBorrowAmount)) // Calculate the "eligible" info (ignoring pending bond reductions) based on the Beacon Chain - _, _, pendingEligibleBorrowedEth, pendingEligibleBondedEth, err := getTrueBorrowAndBondAmounts(rp, bc, nodeAccount.Address) + _, _, pendingEligibleBorrowedEth, pendingEligibleBondedEth, err := getTrueBorrowAndBondAmounts(bc, response.Minipools) if err != nil { return nil, fmt.Errorf("error calculating eligible borrowed and bonded amounts: %w", err) } @@ -482,65 +478,25 @@ func getStatus(c *cli.Command) (*api.NodeStatusResponse, error) { } // Calculate the true borrowed and bonded ETH amounts for a node based on the Beacon status of the minipools -func getTrueBorrowAndBondAmounts(rp *rocketpool.RocketPool, bc beacon.Client, nodeAddress common.Address) (*big.Int, *big.Int, *big.Int, *big.Int, error) { - - mpDetails, err := minipool.GetNodeMinipools(rp, nodeAddress, nil) - if err != nil { - return nil, nil, nil, nil, fmt.Errorf("error loading minipool details: %w", err) - } +func getTrueBorrowAndBondAmounts(bc beacon.Client, minipools []api.MinipoolDetails) (*big.Int, *big.Int, *big.Int, *big.Int, error) { beaconHead, err := bc.GetBeaconHead() if err != nil { return nil, nil, nil, nil, fmt.Errorf("error getting beacon head: %w", err) } - pubkeys := make([]types.ValidatorPubkey, len(mpDetails)) - nodeDeposits := make([]*big.Int, len(mpDetails)) - userDeposits := make([]*big.Int, len(mpDetails)) - pendingNodeDeposits := make([]*big.Int, len(mpDetails)) - pendingUserDeposits := make([]*big.Int, len(mpDetails)) - // Data - var wg errgroup.Group - - for i, mpd := range mpDetails { - if !mpd.Exists { - nodeDeposits[i] = big.NewInt(0) - userDeposits[i] = big.NewInt(0) - pendingNodeDeposits[i] = big.NewInt(0) - pendingUserDeposits[i] = big.NewInt(0) - continue - } - - i := i - address := mpd.Address - pubkeys[i] = mpd.Pubkey - - wg.Go(func() error { - mp, err := minipool.NewMinipool(rp, address, nil) - if err != nil { - return fmt.Errorf("error making binding for minipool %s: %w", address.Hex(), err) - } - - nodeDeposit, err := mp.GetNodeDepositBalance(nil) - if err != nil { - return fmt.Errorf("error getting node deposit for minipool %s: %w", address.Hex(), err) - } - nodeDeposits[i] = nodeDeposit - pendingNodeDeposits[i] = nodeDeposit - - userDeposit, err := mp.GetUserDepositBalance(nil) - if err != nil { - return fmt.Errorf("error getting user deposit for minipool %s: %w", address.Hex(), err) - } - userDeposits[i] = userDeposit - pendingUserDeposits[i] = userDeposit - return nil - }) - } - - // Wait for data - if err = wg.Wait(); err != nil { - return nil, nil, nil, nil, err + pubkeys := make([]types.ValidatorPubkey, len(minipools)) + nodeDeposits := make([]*big.Int, len(minipools)) + userDeposits := make([]*big.Int, len(minipools)) + pendingNodeDeposits := make([]*big.Int, len(minipools)) + pendingUserDeposits := make([]*big.Int, len(minipools)) + + for i, mpd := range minipools { + pubkeys[i] = mpd.ValidatorPubkey + nodeDeposits[i] = mpd.Node.DepositBalance + pendingNodeDeposits[i] = mpd.Node.DepositBalance + userDeposits[i] = mpd.User.DepositBalance + pendingUserDeposits[i] = mpd.User.DepositBalance } statuses, err := bc.GetValidatorStatuses(pubkeys, nil)