Skip to content

Commit a845791

Browse files
committed
Add NetworkStateProvider
1 parent a87400a commit a845791

12 files changed

Lines changed: 8997 additions & 54 deletions

rocketpool/watchtower/generate-rewards-tree.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ func (t *generateRewardsTree) generateRewardsTree(index uint64) {
158158
return
159159
}
160160

161-
var stateManager *state.NetworkStateManager
161+
var stateManager state.NetworkStateProvider
162162

163163
// Try getting the rETH address as a canary to see if the block is available
164164
client := t.rp
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package watchtower
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
"time"
7+
8+
"github.com/ethereum/go-ethereum/common"
9+
"github.com/ethereum/go-ethereum/core/types"
10+
"github.com/rocket-pool/smartnode/bindings/megapool"
11+
"github.com/rocket-pool/smartnode/shared/services/config"
12+
"github.com/rocket-pool/smartnode/shared/services/state"
13+
"github.com/rocket-pool/smartnode/shared/utils/log"
14+
)
15+
16+
const smallStateFixture = "../../shared/services/state/testdata/network_state.json.gz"
17+
18+
// stubRewardSplitCalculator returns a deterministic 50/50 split between
19+
// rETH and node rewards, which is sufficient to verify plumbing.
20+
type stubRewardSplitCalculator struct {
21+
calls []rewardSplitCall
22+
}
23+
type rewardSplitCall struct {
24+
MegapoolAddress common.Address
25+
Rewards *big.Int
26+
}
27+
28+
func (s *stubRewardSplitCalculator) CalculateRewards(megapoolAddress common.Address, rewards *big.Int, _ uint64) (megapool.RewardSplit, error) {
29+
s.calls = append(s.calls, rewardSplitCall{megapoolAddress, new(big.Int).Set(rewards)})
30+
half := new(big.Int).Div(rewards, big.NewInt(2))
31+
remainder := new(big.Int).Sub(rewards, half)
32+
return megapool.RewardSplit{
33+
NodeRewards: half,
34+
VoterRewards: big.NewInt(0),
35+
RethRewards: remainder,
36+
ProtocolDAORewards: big.NewInt(0),
37+
}, nil
38+
}
39+
40+
// stubSmoothingPoolCalculator returns the full smoothing pool balance as the
41+
// rETH share so the test value is deterministic and easy to verify.
42+
type stubSmoothingPoolCalculator struct{}
43+
44+
func (s *stubSmoothingPoolCalculator) GetSmoothingPoolShare(ns *state.NetworkState, _ *types.Header, _ time.Time) (*big.Int, error) {
45+
return ns.NetworkDetails.SmoothingPoolBalance, nil
46+
}
47+
48+
func TestGetNetworkBalancesFromState(t *testing.T) {
49+
provider, err := state.NewStaticNetworkStateProviderFromFile(smallStateFixture)
50+
if err != nil {
51+
t.Fatalf("loading state: %v", err)
52+
}
53+
54+
ns, err := provider.GetHeadState()
55+
if err != nil {
56+
t.Fatalf("GetHeadState: %v", err)
57+
}
58+
59+
logger := log.NewColorLogger(0)
60+
cfg := config.NewRocketPoolConfig("", false)
61+
rewardCalc := &stubRewardSplitCalculator{}
62+
spCalc := &stubSmoothingPoolCalculator{}
63+
64+
elBlockHeader := &types.Header{
65+
Number: new(big.Int).SetUint64(ns.ElBlockNumber),
66+
}
67+
slotTime := time.Unix(int64(ns.BeaconConfig.GenesisTime+ns.BeaconSlotNumber*ns.BeaconConfig.SecondsPerSlot), 0)
68+
69+
task := &submitNetworkBalances{
70+
log: &logger,
71+
cfg: cfg,
72+
}
73+
74+
balances, err := task.getNetworkBalancesFromState(ns, elBlockHeader, slotTime, rewardCalc, spCalc)
75+
if err != nil {
76+
t.Fatalf("getNetworkBalancesFromState: %v", err)
77+
}
78+
79+
if balances.Block != ns.ElBlockNumber {
80+
t.Errorf("Block: got %d, want %d", balances.Block, ns.ElBlockNumber)
81+
}
82+
83+
// DepositPool must equal DepositPoolUserBalance from the state
84+
if balances.DepositPool.Cmp(ns.NetworkDetails.DepositPoolUserBalance) != 0 {
85+
t.Errorf("DepositPool: got %s, want %s", balances.DepositPool, ns.NetworkDetails.DepositPoolUserBalance)
86+
}
87+
88+
// RETHContract must equal RETHBalance from the state
89+
if balances.RETHContract.Cmp(ns.NetworkDetails.RETHBalance) != 0 {
90+
t.Errorf("RETHContract: got %s, want %s", balances.RETHContract, ns.NetworkDetails.RETHBalance)
91+
}
92+
93+
// RETHSupply must equal TotalRETHSupply from the state
94+
if balances.RETHSupply.Cmp(ns.NetworkDetails.TotalRETHSupply) != 0 {
95+
t.Errorf("RETHSupply: got %s, want %s", balances.RETHSupply, ns.NetworkDetails.TotalRETHSupply)
96+
}
97+
98+
// SmoothingPoolShare must equal SmoothingPoolBalance (per our stub)
99+
if balances.SmoothingPoolShare.Cmp(ns.NetworkDetails.SmoothingPoolBalance) != 0 {
100+
t.Errorf("SmoothingPoolShare: got %s, want %s", balances.SmoothingPoolShare, ns.NetworkDetails.SmoothingPoolBalance)
101+
}
102+
103+
// Verify minipool totals are non-negative
104+
if balances.MinipoolsTotal.Sign() < 0 {
105+
t.Errorf("MinipoolsTotal is negative: %s", balances.MinipoolsTotal)
106+
}
107+
if balances.MinipoolsStaking.Sign() < 0 {
108+
t.Errorf("MinipoolsStaking is negative: %s", balances.MinipoolsStaking)
109+
}
110+
// MinipoolsStaking must not exceed MinipoolsTotal
111+
if balances.MinipoolsStaking.Cmp(balances.MinipoolsTotal) > 0 {
112+
t.Errorf("MinipoolsStaking (%s) > MinipoolsTotal (%s)", balances.MinipoolsStaking, balances.MinipoolsTotal)
113+
}
114+
115+
// The fixture has 10 minipools; verify the total is non-zero for staking validators
116+
if len(ns.MinipoolDetails) > 0 && balances.MinipoolsTotal.Sign() == 0 {
117+
t.Error("MinipoolsTotal is zero but the fixture has minipools")
118+
}
119+
120+
// NodeCreditBalance must be the sum of all nodes' DepositCreditBalance
121+
expectedCredit := big.NewInt(0)
122+
for _, node := range ns.NodeDetails {
123+
expectedCredit.Add(expectedCredit, node.DepositCreditBalance)
124+
}
125+
if balances.NodeCreditBalance.Cmp(expectedCredit) != 0 {
126+
t.Errorf("NodeCreditBalance: got %s, want %s", balances.NodeCreditBalance, expectedCredit)
127+
}
128+
129+
// DistributorShareTotal must be the sum of all nodes' DistributorBalanceUserETH
130+
expectedDistributor := big.NewInt(0)
131+
for _, node := range ns.NodeDetails {
132+
expectedDistributor.Add(expectedDistributor, node.DistributorBalanceUserETH)
133+
}
134+
if balances.DistributorShareTotal.Cmp(expectedDistributor) != 0 {
135+
t.Errorf("DistributorShareTotal: got %s, want %s", balances.DistributorShareTotal, expectedDistributor)
136+
}
137+
138+
// Verify megapool fields are non-negative
139+
if balances.MegapoolsUserShareTotal.Sign() < 0 {
140+
t.Errorf("MegapoolsUserShareTotal is negative: %s", balances.MegapoolsUserShareTotal)
141+
}
142+
if balances.MegapoolStaking.Sign() < 0 {
143+
t.Errorf("MegapoolStaking is negative: %s", balances.MegapoolStaking)
144+
}
145+
146+
// The fixture has megapool details; verify they were loaded
147+
if len(ns.MegapoolDetails) == 0 {
148+
t.Fatal("MegapoolDetails is empty — fixture may not have been regenerated with the megapool_details json tag")
149+
}
150+
151+
// MegapoolsUserShareTotal should equal the sum of UserCapital for megapools
152+
// that appear in MegapoolDetails AND have validators in MegapoolToPubkeysMap
153+
// (the balance loop iterates MegapoolDetails, which is keyed by address).
154+
expectedUserCapital := big.NewInt(0)
155+
for addr, mp := range ns.MegapoolDetails {
156+
if _, hasPubkeys := ns.MegapoolToPubkeysMap[addr]; hasPubkeys {
157+
expectedUserCapital.Add(expectedUserCapital, mp.UserCapital)
158+
}
159+
}
160+
if balances.MegapoolsUserShareTotal.Cmp(expectedUserCapital) != 0 {
161+
t.Errorf("MegapoolsUserShareTotal: got %s, want %s (sum of UserCapital for megapools with validators)", balances.MegapoolsUserShareTotal, expectedUserCapital)
162+
}
163+
164+
t.Logf("Balances summary:")
165+
t.Logf(" Block: %d", balances.Block)
166+
t.Logf(" DepositPool: %s", balances.DepositPool)
167+
t.Logf(" MinipoolsTotal: %s", balances.MinipoolsTotal)
168+
t.Logf(" MinipoolsStaking: %s", balances.MinipoolsStaking)
169+
t.Logf(" MegapoolsUserShareTotal: %s", balances.MegapoolsUserShareTotal)
170+
t.Logf(" MegapoolStaking: %s", balances.MegapoolStaking)
171+
t.Logf(" DistributorShareTotal: %s", balances.DistributorShareTotal)
172+
t.Logf(" SmoothingPoolShare: %s", balances.SmoothingPoolShare)
173+
t.Logf(" RETHContract: %s", balances.RETHContract)
174+
t.Logf(" RETHSupply: %s", balances.RETHSupply)
175+
t.Logf(" NodeCreditBalance: %s", balances.NodeCreditBalance)
176+
t.Logf(" RewardCalc calls: %d", len(rewardCalc.calls))
177+
}

rocketpool/watchtower/submit-network-balances.go

Lines changed: 81 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ type storageGetter interface {
4343
GetBool(opts *bind.CallOpts, _key [32]byte) (bool, error)
4444
}
4545

46+
type rewardSplitCalculator interface {
47+
CalculateRewards(megapoolAddress common.Address, rewards *big.Int, elBlockNumber uint64) (megapool.RewardSplit, error)
48+
}
49+
50+
type smoothingPoolShareCalculator interface {
51+
GetSmoothingPoolShare(ns *state.NetworkState, elBlockHeader *types.Header, slotTime time.Time) (*big.Int, error)
52+
}
53+
4654
// Submit network balances task
4755
type submitNetworkBalances struct {
4856
c *cli.Command
@@ -58,6 +66,56 @@ type submitNetworkBalances struct {
5866
isRunning bool
5967
}
6068

69+
// liveRewardSplitCalculator calls the on-chain megapool contract.
70+
type liveRewardSplitCalculator struct {
71+
rp *rocketpool.RocketPool
72+
}
73+
74+
func (c *liveRewardSplitCalculator) CalculateRewards(megapoolAddress common.Address, rewards *big.Int, elBlockNumber uint64) (megapool.RewardSplit, error) {
75+
megapoolContract, err := megapool.NewMegaPoolV1(c.rp, megapoolAddress, nil)
76+
if err != nil {
77+
return megapool.RewardSplit{}, fmt.Errorf("error loading megapool contract: %w", err)
78+
}
79+
opts := &bind.CallOpts{
80+
BlockNumber: new(big.Int).SetUint64(elBlockNumber),
81+
}
82+
return megapoolContract.CalculateRewards(rewards, opts)
83+
}
84+
85+
// liveSmoothingPoolCalculator uses the tree generator to approximate the
86+
// rETH share of the smoothing pool.
87+
type liveSmoothingPoolCalculator struct {
88+
log *log.ColorLogger
89+
cfg *config.RocketPoolConfig
90+
bc beacon.Client
91+
client *rocketpool.RocketPool
92+
}
93+
94+
func (c *liveSmoothingPoolCalculator) GetSmoothingPoolShare(ns *state.NetworkState, elBlockHeader *types.Header, slotTime time.Time) (*big.Int, error) {
95+
currentIndex := ns.NetworkDetails.RewardIndex
96+
startTime := ns.NetworkDetails.IntervalStart
97+
intervalTime := ns.NetworkDetails.IntervalDuration
98+
99+
timeSinceStart := slotTime.Sub(startTime)
100+
intervalsPassed := timeSinceStart / intervalTime
101+
endTime := slotTime
102+
snapshotEnd := &rprewards.SnapshotEnd{
103+
Slot: ns.BeaconSlotNumber,
104+
ConsensusBlock: ns.BeaconSlotNumber,
105+
ExecutionBlock: ns.ElBlockNumber,
106+
}
107+
108+
treegen, err := rprewards.NewTreeGenerator(c.log, "[Balances]", rprewards.NewRewardsExecutionClient(c.client), c.cfg, c.bc, currentIndex, startTime, endTime, snapshotEnd, elBlockHeader, uint64(intervalsPassed), ns)
109+
if err != nil {
110+
return nil, fmt.Errorf("error creating merkle tree generator to approximate share of smoothing pool: %w", err)
111+
}
112+
share, err := treegen.ApproximateStakerShareOfSmoothingPool()
113+
if err != nil {
114+
return nil, fmt.Errorf("error getting approximate share of smoothing pool: %w", err)
115+
}
116+
return share, nil
117+
}
118+
61119
// Network balance info
62120
type networkBalances struct {
63121
Block uint64
@@ -381,11 +439,26 @@ func (t *submitNetworkBalances) getNetworkBalances(elBlockHeader *types.Header,
381439
mgr := state.NewNetworkStateManager(client, t.cfg.Smartnode.GetStateManagerContracts(), t.bc, t.log)
382440

383441
// Create a new state for the target block
384-
state, err := mgr.GetStateForSlot(beaconBlock)
442+
ns, err := mgr.GetStateForSlot(beaconBlock)
385443
if err != nil {
386444
return networkBalances{}, fmt.Errorf("couldn't get network state for EL block %s, Beacon slot %d: %w", elBlock, beaconBlock, err)
387445
}
388446

447+
rewardCalc := &liveRewardSplitCalculator{rp: client}
448+
spCalc := &liveSmoothingPoolCalculator{log: t.log, cfg: t.cfg, bc: t.bc, client: client}
449+
450+
return t.getNetworkBalancesFromState(ns, elBlockHeader, slotTime, rewardCalc, spCalc)
451+
}
452+
453+
// getNetworkBalancesFromState computes the network balances from an already-loaded NetworkState.
454+
func (t *submitNetworkBalances) getNetworkBalancesFromState(
455+
state *state.NetworkState,
456+
elBlockHeader *types.Header,
457+
slotTime time.Time,
458+
rewardCalc rewardSplitCalculator,
459+
spCalc smoothingPoolShareCalculator,
460+
) (networkBalances, error) {
461+
389462
// Data
390463
var wg errgroup.Group
391464
var depositPoolBalance *big.Int
@@ -413,7 +486,8 @@ func (t *submitNetworkBalances) getNetworkBalances(elBlockHeader *types.Header,
413486
megapoolBalanceDetails = make([]megapoolBalanceDetail, len(state.MegapoolDetails))
414487
i := 0
415488
for megapoolAddress, megapoolDetails := range state.MegapoolDetails {
416-
megapoolBalanceDetails[i], err = t.getMegapoolBalanceDetails(megapoolAddress, state, megapoolDetails)
489+
var err error
490+
megapoolBalanceDetails[i], err = t.getMegapoolBalanceDetails(megapoolAddress, state, megapoolDetails, rewardCalc)
417491
if err != nil {
418492
return fmt.Errorf("error getting megapool balance details: %w", err)
419493
}
@@ -434,37 +508,9 @@ func (t *submitNetworkBalances) getNetworkBalances(elBlockHeader *types.Header,
434508

435509
// Get the smoothing pool user share
436510
wg.Go(func() error {
437-
438-
// Get the current interval
439-
currentIndex := state.NetworkDetails.RewardIndex
440-
441-
// Get the start time for the current interval, and how long an interval is supposed to take
442-
startTime := state.NetworkDetails.IntervalStart
443-
intervalTime := state.NetworkDetails.IntervalDuration
444-
445-
timeSinceStart := slotTime.Sub(startTime)
446-
intervalsPassed := timeSinceStart / intervalTime
447-
endTime := slotTime
448-
// Since we aren't generating an actual tree, just use beaconBlock as the snapshotEnd
449-
snapshotEnd := &rprewards.SnapshotEnd{
450-
Slot: beaconBlock,
451-
ConsensusBlock: beaconBlock,
452-
ExecutionBlock: state.ElBlockNumber,
453-
}
454-
455-
// Approximate the staker's share of the smoothing pool balance
456-
// NOTE: this will use the "vanilla" variant of treegen, without rolling records, to retain parity with other Oracle DAO nodes that aren't using rolling records
457-
treegen, err := rprewards.NewTreeGenerator(t.log, "[Balances]", rprewards.NewRewardsExecutionClient(client), t.cfg, t.bc, currentIndex, startTime, endTime, snapshotEnd, elBlockHeader, uint64(intervalsPassed), state)
458-
if err != nil {
459-
return fmt.Errorf("error creating merkle tree generator to approximate share of smoothing pool: %w", err)
460-
}
461-
smoothingPoolShare, err = treegen.ApproximateStakerShareOfSmoothingPool()
462-
if err != nil {
463-
return fmt.Errorf("error getting approximate share of smoothing pool: %w", err)
464-
}
465-
466-
return nil
467-
511+
var err error
512+
smoothingPoolShare, err = spCalc.GetSmoothingPoolShare(state, elBlockHeader, slotTime)
513+
return err
468514
})
469515

470516
// Wait for data
@@ -517,7 +563,7 @@ func (t *submitNetworkBalances) getNetworkBalances(elBlockHeader *types.Header,
517563

518564
}
519565

520-
func (t *submitNetworkBalances) getMegapoolBalanceDetails(megapoolAddress common.Address, state *state.NetworkState, megapoolDetails rpstate.NativeMegapoolDetails) (megapoolBalanceDetail, error) {
566+
func (t *submitNetworkBalances) getMegapoolBalanceDetails(megapoolAddress common.Address, state *state.NetworkState, megapoolDetails rpstate.NativeMegapoolDetails, rewardCalc rewardSplitCalculator) (megapoolBalanceDetail, error) {
521567
megapoolBalanceDetails := megapoolBalanceDetail{}
522568
megapoolValidators := state.MegapoolToPubkeysMap[megapoolAddress]
523569
// iterate the megapoolValidators array
@@ -598,24 +644,9 @@ func (t *submitNetworkBalances) getMegapoolBalanceDetails(megapoolAddress common
598644
beaconBalanceIncrease = beaconBalanceIncrease.Sub(beaconBalanceIncrease, megapoolDetails.NodeBond)
599645
rewards := big.NewInt(0).Add(beaconBalanceIncrease, pendingRewards)
600646

601-
// Load the megapool
602-
megapoolContract, err := megapool.NewMegaPoolV1(t.rp, megapoolAddress, nil)
603-
if err != nil {
604-
return megapoolBalanceDetail{}, fmt.Errorf("error loading megapool contract: %w", err)
605-
}
606-
rewardsSplit := megapool.RewardSplit{
607-
NodeRewards: big.NewInt(0),
608-
VoterRewards: big.NewInt(0),
609-
RethRewards: big.NewInt(0),
610-
ProtocolDAORewards: big.NewInt(0),
611-
}
612647
megapoolBalanceDetails.RethRewards = big.NewInt(0)
613648
if rewards.Cmp(big.NewInt(0)) > 0 {
614-
opts := &bind.CallOpts{
615-
BlockNumber: big.NewInt(0).SetUint64(state.ElBlockNumber),
616-
}
617-
rewardsSplit, err = megapoolContract.CalculateRewards(rewards, opts)
618-
649+
rewardsSplit, err := rewardCalc.CalculateRewards(megapoolAddress, rewards, state.ElBlockNumber)
619650
if err != nil {
620651
return megapoolBalanceDetail{}, fmt.Errorf("error calculating rewards split: %w", err)
621652
}

shared/services/proposals/proposal-manager.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ type ProposalManager struct {
1818
viSnapshotMgr *VotingInfoSnapshotManager
1919
networkTreeMgr *NetworkTreeManager
2020
nodeTreeMgr *NodeTreeManager
21-
stateMgr *state.NetworkStateManager
21+
stateMgr state.NetworkStateProvider
2222

2323
log *log.ColorLogger
2424
logPrefix string

0 commit comments

Comments
 (0)