diff --git a/shared/services/state/manager.go b/shared/services/state/manager.go index 180a397c4..ab217cc01 100644 --- a/shared/services/state/manager.go +++ b/shared/services/state/manager.go @@ -64,7 +64,7 @@ func (m *NetworkStateManager) GetHeadState() (*NetworkState, error) { if err != nil { return nil, fmt.Errorf("error getting latest Beacon slot: %w", err) } - return m.createNetworkState(targetSlot) + return m.createNetworkState(targetSlot, nil) } // Get the state of the network for a single node using the latest Execution layer block, along with the total effective RPL stake for the network @@ -73,12 +73,12 @@ func (m *NetworkStateManager) GetHeadStateForNode(nodeAddress common.Address) (* if err != nil { return nil, fmt.Errorf("error getting latest Beacon slot: %w", err) } - return m.createNetworkStateForNode(targetSlot, nodeAddress) + return m.createNetworkState(targetSlot, []common.Address{nodeAddress}) } // Get the state of the network at the provided Beacon slot func (m *NetworkStateManager) GetStateForSlot(slotNumber uint64) (*NetworkState, error) { - return m.createNetworkState(slotNumber) + return m.createNetworkState(slotNumber, nil) } // Gets the latest valid block diff --git a/shared/services/state/network-state.go b/shared/services/state/network-state.go index 269db7d94..811c4cdfb 100644 --- a/shared/services/state/network-state.go +++ b/shared/services/state/network-state.go @@ -166,8 +166,12 @@ func (ns *NetworkState) UnmarshalJSON(data []byte) error { return nil } -// Creates a snapshot of the entire Rocket Pool network state, on both the Execution and Consensus layers -func (m *NetworkStateManager) createNetworkState(slotNumber uint64) (*NetworkState, error) { +// Creates a snapshot of the Rocket Pool network state, on both the Execution and Consensus layers. +// If nodeAddresses is nil, all nodes are queried. Otherwise, only the specified nodes are included. +func (m *NetworkStateManager) createNetworkState(slotNumber uint64, nodeAddresses []common.Address) (*NetworkState, error) { + allNodes := len(nodeAddresses) == 0 + steps := 9 + currentStep := 0 // Get the execution block for the given slot beaconBlock, exists, err := m.bc.GetBeaconBlock(fmt.Sprintf("%d", slotNumber)) @@ -211,21 +215,46 @@ func (m *NetworkStateManager) createNetworkState(slotNumber uint64) (*NetworkSta if err != nil { return nil, fmt.Errorf("error getting network details: %w", err) } - m.logLine("1/7 - Retrieved network details (%s so far)", time.Since(start)) + currentStep++ + m.logLine("%d/%d - Retrieved network details (%s so far)", currentStep, steps, time.Since(start)) // Node details - state.NodeDetails, err = rpstate.GetAllNativeNodeDetails(m.rp, contracts) - if err != nil { - return nil, fmt.Errorf("error getting all node details: %w", err) + if allNodes { + state.NodeDetails, err = rpstate.GetAllNativeNodeDetails(m.rp, contracts) + if err != nil { + return nil, fmt.Errorf("error getting all node details: %w", err) + } + } else { + state.NodeDetails = make([]rpstate.NativeNodeDetails, 0, len(nodeAddresses)) + for _, addr := range nodeAddresses { + nodeDetails, err := rpstate.GetNativeNodeDetails(m.rp, contracts, addr) + if err != nil { + return nil, fmt.Errorf("error getting node details for %s: %w", addr.Hex(), err) + } + state.NodeDetails = append(state.NodeDetails, nodeDetails) + } } - m.logLine("2/7 - Retrieved node details (%s so far)", time.Since(start)) + currentStep++ + m.logLine("%d/%d - Retrieved node details (%s so far)", currentStep, steps, time.Since(start)) // Minipool details - state.MinipoolDetails, err = rpstate.GetAllNativeMinipoolDetails(m.rp, contracts) - if err != nil { - return nil, fmt.Errorf("error getting all minipool details: %w", err) + if allNodes { + state.MinipoolDetails, err = rpstate.GetAllNativeMinipoolDetails(m.rp, contracts) + if err != nil { + return nil, fmt.Errorf("error getting all minipool details: %w", err) + } + } else { + state.MinipoolDetails = []rpstate.NativeMinipoolDetails{} + for _, addr := range nodeAddresses { + nodeMinipools, err := rpstate.GetNodeNativeMinipoolDetails(m.rp, contracts, addr) + if err != nil { + return nil, fmt.Errorf("error getting minipool details for node %s: %w", addr.Hex(), err) + } + state.MinipoolDetails = append(state.MinipoolDetails, nodeMinipools...) + } } - m.logLine("3/7 - Retrieved minipool details (%s so far)", time.Since(start)) + currentStep++ + m.logLine("%d/%d - Retrieved minipool details (%s so far)", currentStep, steps, time.Since(start)) // Create the node lookup for i, details := range state.NodeDetails { @@ -254,7 +283,8 @@ func (m *NetworkStateManager) createNetworkState(slotNumber uint64) (*NetworkSta if err != nil { return nil, fmt.Errorf("error getting all megapool validator details: %w", err) } - m.logLine("4/7 - Retrieved megapool validator global index (%s so far)", time.Since(start)) + currentStep++ + m.logLine("%d/%d - Retrieved megapool validator global index (%s so far)", currentStep, steps, time.Since(start)) megapoolValidatorPubkeys := make([]types.ValidatorPubkey, 0, len(state.MegapoolValidatorGlobalIndex)) megapoolAddressMap := make(map[common.Address][]types.ValidatorPubkey) @@ -296,7 +326,8 @@ func (m *NetworkStateManager) createNetworkState(slotNumber uint64) (*NetworkSta if err := megapoolWg.Wait(); err != nil { return nil, fmt.Errorf("error getting megapool details: %w", err) } - m.logLine("4/7 - Retrieved megapool validator details (%s so far)", time.Since(start)) + currentStep++ + m.logLine("%d/%d - Retrieved megapool validator details (%s so far)", currentStep, steps, time.Since(start)) // Calculate avg node fees and distributor shares for _, details := range state.NodeDetails { @@ -308,133 +339,8 @@ func (m *NetworkStateManager) createNetworkState(slotNumber uint64) (*NetworkSta if err != nil { return nil, fmt.Errorf("error getting Oracle DAO details: %w", err) } - m.logLine("5/7 - Retrieved Oracle DAO details (%s so far)", time.Since(start)) - - // Get the validator stats from Beacon - statusMap, err := m.bc.GetValidatorStatuses(pubkeys, &beacon.ValidatorStatusOptions{ - Slot: &slotNumber, - }) - if err != nil { - return nil, err - } - state.MinipoolValidatorDetails = statusMap - m.logLine("6/7 - Retrieved validator details (total time: %s)", time.Since(start)) - - // Get the complete node and user shares - mpds := make([]*rpstate.NativeMinipoolDetails, len(state.MinipoolDetails)) - beaconBalances := make([]*big.Int, len(state.MinipoolDetails)) - for i, mpd := range state.MinipoolDetails { - mpds[i] = &state.MinipoolDetails[i] - validator := state.MinipoolValidatorDetails[mpd.Pubkey] - if !validator.Exists { - beaconBalances[i] = big.NewInt(0) - } else { - beaconBalances[i] = eth.GweiToWei(float64(validator.Balance)) - } - } - err = rpstate.CalculateCompleteMinipoolShares(m.rp, contracts, mpds, beaconBalances) - if err != nil { - return nil, err - } - state.MinipoolValidatorDetails = statusMap - m.logLine("7/7 - Calculated complete node and user balance shares (total time: %s)", time.Since(start)) - - return state, nil -} - -// Creates a snapshot of the Rocket Pool network, but only for a single node -func (m *NetworkStateManager) createNetworkStateForNode(slotNumber uint64, nodeAddress common.Address) (*NetworkState, error) { - steps := 7 - - // Get the execution block for the given slot - beaconBlock, exists, err := m.bc.GetBeaconBlock(fmt.Sprintf("%d", slotNumber)) - if err != nil { - return nil, fmt.Errorf("error getting Beacon block for slot %d: %w", slotNumber, err) - } - if !exists { - return nil, fmt.Errorf("slot %d did not have a Beacon block", slotNumber) - } - - // Get the corresponding block on the EL - elBlockNumber := beaconBlock.ExecutionBlockNumber - opts := &bind.CallOpts{ - BlockNumber: big.NewInt(0).SetUint64(elBlockNumber), - } - - beaconConfig, err := m.getBeaconConfig() - if err != nil { - return nil, fmt.Errorf("error getting Beacon config: %w", err) - } - - // Create the state wrapper - state := &NetworkState{ - NodeDetailsByAddress: map[common.Address]*rpstate.NativeNodeDetails{}, - MinipoolDetailsByAddress: map[common.Address]*rpstate.NativeMinipoolDetails{}, - MinipoolDetailsByNode: map[common.Address][]*rpstate.NativeMinipoolDetails{}, - BeaconSlotNumber: slotNumber, - ElBlockNumber: elBlockNumber, - BeaconConfig: *beaconConfig, - } - - m.logLine("Getting network state for EL block %d, Beacon slot %d", elBlockNumber, slotNumber) - start := time.Now() - - // Network contracts and details - contracts, err := rpstate.NewNetworkContracts(m.rp, m.multicaller, m.balanceBatcher, opts) - if err != nil { - return nil, fmt.Errorf("error getting network contracts: %w", err) - } - state.NetworkDetails, err = rpstate.NewNetworkDetails(m.rp, contracts) - if err != nil { - return nil, fmt.Errorf("error getting network details: %w", err) - } - m.logLine("1/%d - Retrieved network details (%s so far)", steps, time.Since(start)) - - // Node details - nodeDetails, err := rpstate.GetNativeNodeDetails(m.rp, contracts, nodeAddress) - if err != nil { - return nil, fmt.Errorf("error getting node details: %w", err) - } - state.NodeDetails = []rpstate.NativeNodeDetails{nodeDetails} - m.logLine("2/%d - Retrieved node details (%s so far)", steps, time.Since(start)) - - // Minipool details - state.MinipoolDetails, err = rpstate.GetNodeNativeMinipoolDetails(m.rp, contracts, nodeAddress) - if err != nil { - return nil, fmt.Errorf("error getting all minipool details: %w", err) - } - m.logLine("3/%d - Retrieved minipool details (%s so far)", steps, time.Since(start)) - - // Create the node lookup - for i, details := range state.NodeDetails { - state.NodeDetailsByAddress[details.NodeAddress] = &state.NodeDetails[i] - } - - // Create the minipool lookups - pubkeys := make([]types.ValidatorPubkey, 0, len(state.MinipoolDetails)) - emptyPubkey := types.ValidatorPubkey{} - for i, details := range state.MinipoolDetails { - state.MinipoolDetailsByAddress[details.MinipoolAddress] = &state.MinipoolDetails[i] - if details.Pubkey != emptyPubkey { - pubkeys = append(pubkeys, details.Pubkey) - } - - // The map of nodes to minipools - nodeList, exists := state.MinipoolDetailsByNode[details.NodeAddress] - if !exists { - nodeList = []*rpstate.NativeMinipoolDetails{} - } - nodeList = append(nodeList, &state.MinipoolDetails[i]) - state.MinipoolDetailsByNode[details.NodeAddress] = nodeList - } - - // Calculate avg node fees and distributor shares - for _, details := range state.NodeDetails { - details.CalculateAverageFeeAndDistributorShares(state.MinipoolDetailsByNode[details.NodeAddress]) - } - - // Get the total network effective RPL stake - currentStep := 4 + currentStep++ + m.logLine("%d/%d - Retrieved Oracle DAO details (%s so far)", currentStep, steps, time.Since(start)) // Get the validator stats from Beacon statusMap, err := m.bc.GetValidatorStatuses(pubkeys, &beacon.ValidatorStatusOptions{ @@ -444,8 +350,8 @@ func (m *NetworkStateManager) createNetworkStateForNode(slotNumber uint64, nodeA return nil, err } state.MinipoolValidatorDetails = statusMap - m.logLine("%d/%d - Retrieved validator details (total time: %s)", currentStep, steps, time.Since(start)) currentStep++ + m.logLine("%d/%d - Retrieved validator details (%s so far)", currentStep, steps, time.Since(start)) // Get the complete node and user shares mpds := make([]*rpstate.NativeMinipoolDetails, len(state.MinipoolDetails)) @@ -464,65 +370,16 @@ func (m *NetworkStateManager) createNetworkStateForNode(slotNumber uint64, nodeA return nil, err } state.MinipoolValidatorDetails = statusMap - m.logLine("%d/%d - Calculated complete node and user balance shares (total time: %s)", currentStep, steps, time.Since(start)) currentStep++ + m.logLine("%d/%d - Calculated complete node and user balance shares (%s so far)", currentStep, steps, time.Since(start)) - // Get the protocol DAO proposals + // Protocol DAO proposals state.ProtocolDaoProposalDetails, err = rpstate.GetAllProtocolDaoProposalDetails(m.rp, contracts) if err != nil { return nil, fmt.Errorf("error getting Protocol DAO proposal details: %w", err) } - m.logLine("%d/%d - Retrieved Protocol DAO proposals (total time: %s)", currentStep, steps, time.Since(start)) - currentStep++ - - state.MegapoolValidatorGlobalIndex, err = rpstate.GetAllMegapoolValidators(m.rp, contracts) - if err != nil { - return nil, fmt.Errorf("error getting all megapool validator details: %w", err) - } - - megapoolValidatorPubkeys := make([]types.ValidatorPubkey, 0, len(state.MegapoolValidatorGlobalIndex)) - megapoolAddressMap := make(map[common.Address][]types.ValidatorPubkey) - megapoolValidatorInfo := make(map[types.ValidatorPubkey]*megapool.ValidatorInfoFromGlobalIndex) - for i := range state.MegapoolValidatorGlobalIndex { - validator := &state.MegapoolValidatorGlobalIndex[i] - if len(validator.Pubkey) > 0 { - pubkey := types.ValidatorPubkey(validator.Pubkey) - megapoolAddressMap[validator.MegapoolAddress] = append(megapoolAddressMap[validator.MegapoolAddress], pubkey) - megapoolValidatorPubkeys = append(megapoolValidatorPubkeys, pubkey) - megapoolValidatorInfo[pubkey] = validator - } - } - state.MegapoolToPubkeysMap = megapoolAddressMap - state.MegapoolValidatorInfo = megapoolValidatorInfo - - megapoolAddresses := make([]common.Address, 0, len(megapoolAddressMap)) - for addr := range megapoolAddressMap { - megapoolAddresses = append(megapoolAddresses, addr) - } - - // Fetch beacon validator statuses and EL megapool details in parallel - var megapoolWg errgroup.Group - megapoolWg.Go(func() error { - statusMap, err := m.bc.GetValidatorStatuses(megapoolValidatorPubkeys, &beacon.ValidatorStatusOptions{ - Slot: &slotNumber, - }) - if err != nil { - return err - } - state.MegapoolValidatorDetails = statusMap - return nil - }) - megapoolWg.Go(func() error { - var err error - state.MegapoolDetails, err = rpstate.GetBulkMegapoolDetails(m.rp, contracts, megapoolAddresses) - return err - }) - if err := megapoolWg.Wait(); err != nil { - return nil, fmt.Errorf("error getting megapool details: %w", err) - } - m.logLine("%d/%d - Retrieved megapool validator details (total time: %s)", currentStep, steps, time.Since(start)) - currentStep++ + m.logLine("%d/%d - Retrieved Protocol DAO proposals (total time: %s)", currentStep, steps, time.Since(start)) return state, nil } diff --git a/shared/services/state/network-state_test.go b/shared/services/state/network-state_test.go new file mode 100644 index 000000000..14fdfb29b --- /dev/null +++ b/shared/services/state/network-state_test.go @@ -0,0 +1,502 @@ +package state + +import ( + "encoding/json" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/rocket-pool/smartnode/bindings/dao/protocol" + "github.com/rocket-pool/smartnode/bindings/megapool" + "github.com/rocket-pool/smartnode/bindings/types" + rpstate "github.com/rocket-pool/smartnode/bindings/utils/state" + "github.com/rocket-pool/smartnode/shared/services/beacon" +) + +func newTestPubkey(b byte) types.ValidatorPubkey { + var pk types.ValidatorPubkey + pk[0] = b + return pk +} + +func buildTestState() *NetworkState { + nodeAddrA := common.HexToAddress("0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + nodeAddrB := common.HexToAddress("0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + mpAddrA1 := common.HexToAddress("0xA100000000000000000000000000000000000000") + mpAddrA2 := common.HexToAddress("0xA200000000000000000000000000000000000000") + mpAddrB1 := common.HexToAddress("0xB100000000000000000000000000000000000000") + megapoolAddrA := common.HexToAddress("0xAA00000000000000000000000000000000000000") + + pubkeyA1 := newTestPubkey(0xA1) + pubkeyA2 := newTestPubkey(0xA2) + pubkeyB1 := newTestPubkey(0xB1) + megapoolPubkey := newTestPubkey(0xCC) + + nodeDetails := []rpstate.NativeNodeDetails{ + { + Exists: true, + NodeAddress: nodeAddrA, + RegistrationTime: big.NewInt(1000), + RewardNetwork: big.NewInt(0), + LegacyStakedRPL: big.NewInt(100), + EffectiveRPLStake: big.NewInt(100), + MinimumRPLStake: big.NewInt(10), + MaximumRPLStake: big.NewInt(1000), + EthBorrowed: big.NewInt(0), + EthBorrowedLimit: big.NewInt(0), + MegapoolETHBorrowed: big.NewInt(0), + MinipoolETHBorrowed: big.NewInt(0), + EthBonded: big.NewInt(0), + MegapoolEthBonded: big.NewInt(0), + MinipoolETHBonded: big.NewInt(0), + MegapoolStakedRPL: big.NewInt(0), + UnstakingRPL: big.NewInt(0), + LockedRPL: big.NewInt(0), + MinipoolCount: big.NewInt(2), + BalanceETH: big.NewInt(0), + BalanceRETH: big.NewInt(0), + BalanceRPL: big.NewInt(0), + BalanceOldRPL: big.NewInt(0), + DepositCreditBalance: big.NewInt(0), + DistributorBalanceUserETH: big.NewInt(0), + DistributorBalanceNodeETH: big.NewInt(0), + SmoothingPoolRegistrationChanged: big.NewInt(0), + AverageNodeFee: big.NewInt(0), + CollateralisationRatio: big.NewInt(0), + DistributorBalance: big.NewInt(0), + MegapoolAddress: megapoolAddrA, + MegapoolDeployed: true, + }, + { + Exists: true, + NodeAddress: nodeAddrB, + RegistrationTime: big.NewInt(2000), + RewardNetwork: big.NewInt(0), + LegacyStakedRPL: big.NewInt(200), + EffectiveRPLStake: big.NewInt(200), + MinimumRPLStake: big.NewInt(20), + MaximumRPLStake: big.NewInt(2000), + EthBorrowed: big.NewInt(0), + EthBorrowedLimit: big.NewInt(0), + MegapoolETHBorrowed: big.NewInt(0), + MinipoolETHBorrowed: big.NewInt(0), + EthBonded: big.NewInt(0), + MegapoolEthBonded: big.NewInt(0), + MinipoolETHBonded: big.NewInt(0), + MegapoolStakedRPL: big.NewInt(0), + UnstakingRPL: big.NewInt(0), + LockedRPL: big.NewInt(0), + MinipoolCount: big.NewInt(1), + BalanceETH: big.NewInt(0), + BalanceRETH: big.NewInt(0), + BalanceRPL: big.NewInt(0), + BalanceOldRPL: big.NewInt(0), + DepositCreditBalance: big.NewInt(0), + DistributorBalanceUserETH: big.NewInt(0), + DistributorBalanceNodeETH: big.NewInt(0), + SmoothingPoolRegistrationChanged: big.NewInt(0), + AverageNodeFee: big.NewInt(0), + CollateralisationRatio: big.NewInt(0), + DistributorBalance: big.NewInt(0), + }, + } + + minipoolDetails := []rpstate.NativeMinipoolDetails{ + { + Exists: true, + MinipoolAddress: mpAddrA1, + Pubkey: pubkeyA1, + NodeAddress: nodeAddrA, + NodeFee: big.NewInt(1e17), + NodeDepositBalance: big.NewInt(16_000_000_000), + UserDepositBalance: big.NewInt(16_000_000_000), + StatusTime: big.NewInt(1000), + StatusBlock: big.NewInt(100), + Balance: big.NewInt(0), + DistributableBalance: big.NewInt(0), + NodeShareOfBalance: big.NewInt(0), + UserShareOfBalance: big.NewInt(0), + NodeRefundBalance: big.NewInt(0), + PenaltyCount: big.NewInt(0), + PenaltyRate: big.NewInt(0), + UserDepositAssignedTime: big.NewInt(1000), + NodeShareOfBalanceIncludingBeacon: big.NewInt(0), + UserShareOfBalanceIncludingBeacon: big.NewInt(0), + NodeShareOfBeaconBalance: big.NewInt(0), + UserShareOfBeaconBalance: big.NewInt(0), + LastBondReductionTime: big.NewInt(0), + LastBondReductionPrevValue: big.NewInt(0), + LastBondReductionPrevNodeFee: big.NewInt(0), + ReduceBondTime: big.NewInt(0), + ReduceBondValue: big.NewInt(0), + PreMigrationBalance: big.NewInt(0), + Status: types.Staking, + }, + { + Exists: true, + MinipoolAddress: mpAddrA2, + Pubkey: pubkeyA2, + NodeAddress: nodeAddrA, + NodeFee: big.NewInt(1e17), + NodeDepositBalance: big.NewInt(8_000_000_000), + UserDepositBalance: big.NewInt(24_000_000_000), + StatusTime: big.NewInt(1100), + StatusBlock: big.NewInt(110), + Balance: big.NewInt(0), + DistributableBalance: big.NewInt(0), + NodeShareOfBalance: big.NewInt(0), + UserShareOfBalance: big.NewInt(0), + NodeRefundBalance: big.NewInt(0), + PenaltyCount: big.NewInt(0), + PenaltyRate: big.NewInt(0), + UserDepositAssignedTime: big.NewInt(1100), + NodeShareOfBalanceIncludingBeacon: big.NewInt(0), + UserShareOfBalanceIncludingBeacon: big.NewInt(0), + NodeShareOfBeaconBalance: big.NewInt(0), + UserShareOfBeaconBalance: big.NewInt(0), + LastBondReductionTime: big.NewInt(0), + LastBondReductionPrevValue: big.NewInt(0), + LastBondReductionPrevNodeFee: big.NewInt(0), + ReduceBondTime: big.NewInt(0), + ReduceBondValue: big.NewInt(0), + PreMigrationBalance: big.NewInt(0), + Status: types.Staking, + }, + { + Exists: true, + MinipoolAddress: mpAddrB1, + Pubkey: pubkeyB1, + NodeAddress: nodeAddrB, + NodeFee: big.NewInt(5e16), + NodeDepositBalance: big.NewInt(16_000_000_000), + UserDepositBalance: big.NewInt(16_000_000_000), + StatusTime: big.NewInt(2000), + StatusBlock: big.NewInt(200), + Balance: big.NewInt(0), + DistributableBalance: big.NewInt(0), + NodeShareOfBalance: big.NewInt(0), + UserShareOfBalance: big.NewInt(0), + NodeRefundBalance: big.NewInt(0), + PenaltyCount: big.NewInt(0), + PenaltyRate: big.NewInt(0), + UserDepositAssignedTime: big.NewInt(2000), + NodeShareOfBalanceIncludingBeacon: big.NewInt(0), + UserShareOfBalanceIncludingBeacon: big.NewInt(0), + NodeShareOfBeaconBalance: big.NewInt(0), + UserShareOfBeaconBalance: big.NewInt(0), + LastBondReductionTime: big.NewInt(0), + LastBondReductionPrevValue: big.NewInt(0), + LastBondReductionPrevNodeFee: big.NewInt(0), + ReduceBondTime: big.NewInt(0), + ReduceBondValue: big.NewInt(0), + PreMigrationBalance: big.NewInt(0), + Status: types.Staking, + }, + } + + megapoolValidatorGlobalIndex := []megapool.ValidatorInfoFromGlobalIndex{ + { + Pubkey: megapoolPubkey[:], + MegapoolAddress: megapoolAddrA, + ValidatorId: 1, + ValidatorInfo: megapool.ValidatorInfo{ + Staked: true, + }, + }, + } + + nodeDetailsByAddress := map[common.Address]*rpstate.NativeNodeDetails{ + nodeAddrA: &nodeDetails[0], + nodeAddrB: &nodeDetails[1], + } + + minipoolDetailsByAddress := map[common.Address]*rpstate.NativeMinipoolDetails{ + mpAddrA1: &minipoolDetails[0], + mpAddrA2: &minipoolDetails[1], + mpAddrB1: &minipoolDetails[2], + } + + minipoolDetailsByNode := map[common.Address][]*rpstate.NativeMinipoolDetails{ + nodeAddrA: {&minipoolDetails[0], &minipoolDetails[1]}, + nodeAddrB: {&minipoolDetails[2]}, + } + + megapoolToPubkeys := map[common.Address][]types.ValidatorPubkey{ + megapoolAddrA: {megapoolPubkey}, + } + + megapoolValidatorInfo := map[types.ValidatorPubkey]*megapool.ValidatorInfoFromGlobalIndex{ + megapoolPubkey: &megapoolValidatorGlobalIndex[0], + } + + megapoolDetails := map[common.Address]rpstate.NativeMegapoolDetails{ + megapoolAddrA: { + Address: megapoolAddrA, + Deployed: true, + ActiveValidatorCount: 1, + UserCapital: big.NewInt(24_000_000_000), + NodeBond: big.NewInt(8_000_000_000), + NodeDebt: big.NewInt(0), + RefundValue: big.NewInt(0), + AssignedValue: big.NewInt(0), + BondRequirement: big.NewInt(0), + EthBalance: big.NewInt(0), + PendingRewards: big.NewInt(0), + NodeQueuedBond: big.NewInt(0), + }, + } + + return &NetworkState{ + ElBlockNumber: 1000, + BeaconSlotNumber: 32000, + BeaconConfig: beacon.Eth2Config{ + GenesisTime: 1600000000, + SecondsPerSlot: 12, + SlotsPerEpoch: 32, + }, + NetworkDetails: &rpstate.NetworkDetails{ + RplPrice: big.NewInt(1e16), + MinCollateralFraction: big.NewInt(1e17), + MaxCollateralFraction: big.NewInt(15e17), + IntervalDuration: 28 * 24 * time.Hour, + NodeOperatorRewardsPercent: big.NewInt(0), + TrustedNodeOperatorRewardsPercent: big.NewInt(0), + ProtocolDaoRewardsPercent: big.NewInt(0), + }, + NodeDetails: nodeDetails, + NodeDetailsByAddress: nodeDetailsByAddress, + MinipoolDetails: minipoolDetails, + MinipoolDetailsByAddress: minipoolDetailsByAddress, + MinipoolDetailsByNode: minipoolDetailsByNode, + MinipoolValidatorDetails: ValidatorDetailsMap{ + pubkeyA1: {Pubkey: pubkeyA1, Index: "1", Exists: true, Balance: 32000000000, ActivationEpoch: 0, ExitEpoch: ^uint64(0)}, + pubkeyA2: {Pubkey: pubkeyA2, Index: "2", Exists: true, Balance: 32000000000, ActivationEpoch: 0, ExitEpoch: ^uint64(0)}, + pubkeyB1: {Pubkey: pubkeyB1, Index: "3", Exists: true, Balance: 32000000000, ActivationEpoch: 0, ExitEpoch: ^uint64(0)}, + }, + MegapoolValidatorDetails: ValidatorDetailsMap{ + megapoolPubkey: {Pubkey: megapoolPubkey, Index: "4", Exists: true, Balance: 32000000000, ActivationEpoch: 0, ExitEpoch: ^uint64(0)}, + }, + MegapoolValidatorGlobalIndex: megapoolValidatorGlobalIndex, + MegapoolToPubkeysMap: megapoolToPubkeys, + MegapoolValidatorInfo: megapoolValidatorInfo, + MegapoolDetails: megapoolDetails, + OracleDaoMemberDetails: []rpstate.OracleDaoMemberDetails{ + { + Address: nodeAddrA, + Exists: true, + ID: "odao-a", + RPLBondAmount: big.NewInt(1000), + JoinedTime: time.Unix(1000, 0), + LastProposalTime: time.Unix(1000, 0), + }, + }, + ProtocolDaoProposalDetails: []protocol.ProtocolDaoProposalDetails{ + { + ID: 1, + ProposerAddress: nodeAddrA, + VotingPowerRequired: big.NewInt(100), + VotingPowerFor: big.NewInt(80), + VotingPowerAgainst: big.NewInt(10), + VotingPowerAbstained: big.NewInt(10), + }, + }, + } +} + +// TestNetworkStateJSONRoundtrip verifies that marshaling and unmarshaling +// a NetworkState preserves all serialized fields and correctly rebuilds +// the index maps (NodeDetailsByAddress, MinipoolDetailsByAddress, +// MinipoolDetailsByNode) that are excluded from JSON. +func TestNetworkStateJSONRoundtrip(t *testing.T) { + original := buildTestState() + + data, err := json.Marshal(original) + if err != nil { + t.Fatalf("marshal failed: %v", err) + } + + var restored NetworkState + if err := json.Unmarshal(data, &restored); err != nil { + t.Fatalf("unmarshal failed: %v", err) + } + + // Scalar fields + if restored.ElBlockNumber != original.ElBlockNumber { + t.Errorf("ElBlockNumber mismatch: got %d, want %d", restored.ElBlockNumber, original.ElBlockNumber) + } + if restored.BeaconSlotNumber != original.BeaconSlotNumber { + t.Errorf("BeaconSlotNumber mismatch: got %d, want %d", restored.BeaconSlotNumber, original.BeaconSlotNumber) + } + + // Node details + if len(restored.NodeDetails) != len(original.NodeDetails) { + t.Fatalf("NodeDetails count: got %d, want %d", len(restored.NodeDetails), len(original.NodeDetails)) + } + + // NodeDetailsByAddress rebuilt + if len(restored.NodeDetailsByAddress) != len(original.NodeDetailsByAddress) { + t.Fatalf("NodeDetailsByAddress count: got %d, want %d", len(restored.NodeDetailsByAddress), len(original.NodeDetailsByAddress)) + } + for addr := range original.NodeDetailsByAddress { + if _, ok := restored.NodeDetailsByAddress[addr]; !ok { + t.Errorf("NodeDetailsByAddress missing key %s", addr.Hex()) + } + } + + // Verify NodeDetailsByAddress points into the NodeDetails slice + for addr, ptr := range restored.NodeDetailsByAddress { + found := false + for i := range restored.NodeDetails { + if &restored.NodeDetails[i] == ptr { + found = true + break + } + } + if !found { + t.Errorf("NodeDetailsByAddress[%s] does not point into NodeDetails slice", addr.Hex()) + } + } + + // Minipool details + if len(restored.MinipoolDetails) != len(original.MinipoolDetails) { + t.Fatalf("MinipoolDetails count: got %d, want %d", len(restored.MinipoolDetails), len(original.MinipoolDetails)) + } + + // MinipoolDetailsByAddress rebuilt + if len(restored.MinipoolDetailsByAddress) != len(original.MinipoolDetailsByAddress) { + t.Fatalf("MinipoolDetailsByAddress count: got %d, want %d", len(restored.MinipoolDetailsByAddress), len(original.MinipoolDetailsByAddress)) + } + for addr := range original.MinipoolDetailsByAddress { + if _, ok := restored.MinipoolDetailsByAddress[addr]; !ok { + t.Errorf("MinipoolDetailsByAddress missing key %s", addr.Hex()) + } + } + + // MinipoolDetailsByNode rebuilt + nodeAddrA := common.HexToAddress("0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + nodeAddrB := common.HexToAddress("0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") + if len(restored.MinipoolDetailsByNode) != 2 { + t.Fatalf("MinipoolDetailsByNode count: got %d, want 2", len(restored.MinipoolDetailsByNode)) + } + if len(restored.MinipoolDetailsByNode[nodeAddrA]) != 2 { + t.Errorf("MinipoolDetailsByNode[nodeA] count: got %d, want 2", len(restored.MinipoolDetailsByNode[nodeAddrA])) + } + if len(restored.MinipoolDetailsByNode[nodeAddrB]) != 1 { + t.Errorf("MinipoolDetailsByNode[nodeB] count: got %d, want 1", len(restored.MinipoolDetailsByNode[nodeAddrB])) + } + + // Minipool validator details + if len(restored.MinipoolValidatorDetails) != len(original.MinipoolValidatorDetails) { + t.Errorf("MinipoolValidatorDetails count: got %d, want %d", + len(restored.MinipoolValidatorDetails), len(original.MinipoolValidatorDetails)) + } + + // Megapool validator details + if len(restored.MegapoolValidatorDetails) != len(original.MegapoolValidatorDetails) { + t.Errorf("MegapoolValidatorDetails count: got %d, want %d", + len(restored.MegapoolValidatorDetails), len(original.MegapoolValidatorDetails)) + } + + // Megapool validator global index + if len(restored.MegapoolValidatorGlobalIndex) != len(original.MegapoolValidatorGlobalIndex) { + t.Errorf("MegapoolValidatorGlobalIndex count: got %d, want %d", + len(restored.MegapoolValidatorGlobalIndex), len(original.MegapoolValidatorGlobalIndex)) + } + + // Oracle DAO member details + if len(restored.OracleDaoMemberDetails) != len(original.OracleDaoMemberDetails) { + t.Errorf("OracleDaoMemberDetails count: got %d, want %d", + len(restored.OracleDaoMemberDetails), len(original.OracleDaoMemberDetails)) + } + if restored.OracleDaoMemberDetails[0].ID != "odao-a" { + t.Errorf("OracleDaoMemberDetails[0].ID: got %q, want %q", restored.OracleDaoMemberDetails[0].ID, "odao-a") + } + + // Protocol DAO proposal details + if len(restored.ProtocolDaoProposalDetails) != len(original.ProtocolDaoProposalDetails) { + t.Errorf("ProtocolDaoProposalDetails count: got %d, want %d", + len(restored.ProtocolDaoProposalDetails), len(original.ProtocolDaoProposalDetails)) + } + if restored.ProtocolDaoProposalDetails[0].ID != 1 { + t.Errorf("ProtocolDaoProposalDetails[0].ID: got %d, want 1", restored.ProtocolDaoProposalDetails[0].ID) + } +} + +// TestUnmarshalDuplicateNodeErrors verifies that unmarshaling a +// NetworkState with duplicate node addresses produces an error. +func TestUnmarshalDuplicateNodeErrors(t *testing.T) { + addr := common.HexToAddress("0x1111111111111111111111111111111111111111") + state := &NetworkState{ + BeaconConfig: beacon.Eth2Config{}, + NetworkDetails: &rpstate.NetworkDetails{ + RplPrice: big.NewInt(0), + MinCollateralFraction: big.NewInt(0), + MaxCollateralFraction: big.NewInt(0), + NodeOperatorRewardsPercent: big.NewInt(0), + TrustedNodeOperatorRewardsPercent: big.NewInt(0), + ProtocolDaoRewardsPercent: big.NewInt(0), + }, + NodeDetails: []rpstate.NativeNodeDetails{ + {NodeAddress: addr, RegistrationTime: big.NewInt(0), RewardNetwork: big.NewInt(0), LegacyStakedRPL: big.NewInt(0), EffectiveRPLStake: big.NewInt(0), MinimumRPLStake: big.NewInt(0), MaximumRPLStake: big.NewInt(0), EthBorrowed: big.NewInt(0), EthBorrowedLimit: big.NewInt(0), MegapoolETHBorrowed: big.NewInt(0), MinipoolETHBorrowed: big.NewInt(0), EthBonded: big.NewInt(0), MegapoolEthBonded: big.NewInt(0), MinipoolETHBonded: big.NewInt(0), MegapoolStakedRPL: big.NewInt(0), UnstakingRPL: big.NewInt(0), LockedRPL: big.NewInt(0), MinipoolCount: big.NewInt(0), BalanceETH: big.NewInt(0), BalanceRETH: big.NewInt(0), BalanceRPL: big.NewInt(0), BalanceOldRPL: big.NewInt(0), DepositCreditBalance: big.NewInt(0), DistributorBalanceUserETH: big.NewInt(0), DistributorBalanceNodeETH: big.NewInt(0), SmoothingPoolRegistrationChanged: big.NewInt(0), AverageNodeFee: big.NewInt(0), CollateralisationRatio: big.NewInt(0), DistributorBalance: big.NewInt(0)}, + {NodeAddress: addr, RegistrationTime: big.NewInt(0), RewardNetwork: big.NewInt(0), LegacyStakedRPL: big.NewInt(0), EffectiveRPLStake: big.NewInt(0), MinimumRPLStake: big.NewInt(0), MaximumRPLStake: big.NewInt(0), EthBorrowed: big.NewInt(0), EthBorrowedLimit: big.NewInt(0), MegapoolETHBorrowed: big.NewInt(0), MinipoolETHBorrowed: big.NewInt(0), EthBonded: big.NewInt(0), MegapoolEthBonded: big.NewInt(0), MinipoolETHBonded: big.NewInt(0), MegapoolStakedRPL: big.NewInt(0), UnstakingRPL: big.NewInt(0), LockedRPL: big.NewInt(0), MinipoolCount: big.NewInt(0), BalanceETH: big.NewInt(0), BalanceRETH: big.NewInt(0), BalanceRPL: big.NewInt(0), BalanceOldRPL: big.NewInt(0), DepositCreditBalance: big.NewInt(0), DistributorBalanceUserETH: big.NewInt(0), DistributorBalanceNodeETH: big.NewInt(0), SmoothingPoolRegistrationChanged: big.NewInt(0), AverageNodeFee: big.NewInt(0), CollateralisationRatio: big.NewInt(0), DistributorBalance: big.NewInt(0)}, + }, + MinipoolDetails: []rpstate.NativeMinipoolDetails{}, + MinipoolValidatorDetails: ValidatorDetailsMap{}, + MegapoolValidatorDetails: ValidatorDetailsMap{}, + } + + data, err := json.Marshal(state) + if err != nil { + t.Fatalf("marshal failed: %v", err) + } + + var restored NetworkState + err = json.Unmarshal(data, &restored) + if err == nil { + t.Fatal("expected error for duplicate node address, got nil") + } +} + +// TestUnmarshalDuplicateMinipoolErrors verifies that unmarshaling a +// NetworkState with duplicate minipool addresses produces an error. +func TestUnmarshalDuplicateMinipoolErrors(t *testing.T) { + mpAddr := common.HexToAddress("0x2222222222222222222222222222222222222222") + nodeAddr := common.HexToAddress("0x3333333333333333333333333333333333333333") + pk1 := newTestPubkey(0x01) + pk2 := newTestPubkey(0x02) + + zeroInt := func() *big.Int { return big.NewInt(0) } + + state := &NetworkState{ + BeaconConfig: beacon.Eth2Config{}, + NetworkDetails: &rpstate.NetworkDetails{ + RplPrice: zeroInt(), + MinCollateralFraction: zeroInt(), + MaxCollateralFraction: zeroInt(), + NodeOperatorRewardsPercent: zeroInt(), + TrustedNodeOperatorRewardsPercent: zeroInt(), + ProtocolDaoRewardsPercent: zeroInt(), + }, + NodeDetails: []rpstate.NativeNodeDetails{ + {NodeAddress: nodeAddr, RegistrationTime: zeroInt(), RewardNetwork: zeroInt(), LegacyStakedRPL: zeroInt(), EffectiveRPLStake: zeroInt(), MinimumRPLStake: zeroInt(), MaximumRPLStake: zeroInt(), EthBorrowed: zeroInt(), EthBorrowedLimit: zeroInt(), MegapoolETHBorrowed: zeroInt(), MinipoolETHBorrowed: zeroInt(), EthBonded: zeroInt(), MegapoolEthBonded: zeroInt(), MinipoolETHBonded: zeroInt(), MegapoolStakedRPL: zeroInt(), UnstakingRPL: zeroInt(), LockedRPL: zeroInt(), MinipoolCount: zeroInt(), BalanceETH: zeroInt(), BalanceRETH: zeroInt(), BalanceRPL: zeroInt(), BalanceOldRPL: zeroInt(), DepositCreditBalance: zeroInt(), DistributorBalanceUserETH: zeroInt(), DistributorBalanceNodeETH: zeroInt(), SmoothingPoolRegistrationChanged: zeroInt(), AverageNodeFee: zeroInt(), CollateralisationRatio: zeroInt(), DistributorBalance: zeroInt()}, + }, + MinipoolDetails: []rpstate.NativeMinipoolDetails{ + {Exists: true, MinipoolAddress: mpAddr, Pubkey: pk1, NodeAddress: nodeAddr, NodeFee: zeroInt(), NodeDepositBalance: zeroInt(), UserDepositBalance: zeroInt(), StatusTime: zeroInt(), StatusBlock: zeroInt(), Balance: zeroInt(), DistributableBalance: zeroInt(), NodeShareOfBalance: zeroInt(), UserShareOfBalance: zeroInt(), NodeRefundBalance: zeroInt(), PenaltyCount: zeroInt(), PenaltyRate: zeroInt(), UserDepositAssignedTime: zeroInt(), NodeShareOfBalanceIncludingBeacon: zeroInt(), UserShareOfBalanceIncludingBeacon: zeroInt(), NodeShareOfBeaconBalance: zeroInt(), UserShareOfBeaconBalance: zeroInt(), LastBondReductionTime: zeroInt(), LastBondReductionPrevValue: zeroInt(), LastBondReductionPrevNodeFee: zeroInt(), ReduceBondTime: zeroInt(), ReduceBondValue: zeroInt(), PreMigrationBalance: zeroInt()}, + {Exists: true, MinipoolAddress: mpAddr, Pubkey: pk2, NodeAddress: nodeAddr, NodeFee: zeroInt(), NodeDepositBalance: zeroInt(), UserDepositBalance: zeroInt(), StatusTime: zeroInt(), StatusBlock: zeroInt(), Balance: zeroInt(), DistributableBalance: zeroInt(), NodeShareOfBalance: zeroInt(), UserShareOfBalance: zeroInt(), NodeRefundBalance: zeroInt(), PenaltyCount: zeroInt(), PenaltyRate: zeroInt(), UserDepositAssignedTime: zeroInt(), NodeShareOfBalanceIncludingBeacon: zeroInt(), UserShareOfBalanceIncludingBeacon: zeroInt(), NodeShareOfBeaconBalance: zeroInt(), UserShareOfBeaconBalance: zeroInt(), LastBondReductionTime: zeroInt(), LastBondReductionPrevValue: zeroInt(), LastBondReductionPrevNodeFee: zeroInt(), ReduceBondTime: zeroInt(), ReduceBondValue: zeroInt(), PreMigrationBalance: zeroInt()}, + }, + MinipoolValidatorDetails: ValidatorDetailsMap{}, + MegapoolValidatorDetails: ValidatorDetailsMap{}, + } + + data, err := json.Marshal(state) + if err != nil { + t.Fatalf("marshal failed: %v", err) + } + + var restored NetworkState + err = json.Unmarshal(data, &restored) + if err == nil { + t.Fatal("expected error for duplicate minipool address, got nil") + } +}