Skip to content

Commit cfb2d63

Browse files
committed
Add generator v8 mainnet unit test
1 parent 29be49f commit cfb2d63

8 files changed

Lines changed: 762 additions & 0 deletions
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package rewards
2+
3+
import (
4+
"fmt"
5+
"math/big"
6+
"os"
7+
"path/filepath"
8+
"testing"
9+
10+
"github.com/ethereum/go-ethereum/common"
11+
"github.com/ethereum/go-ethereum/core/types"
12+
"github.com/fatih/color"
13+
"github.com/rocket-pool/smartnode/shared/services/beacon"
14+
"github.com/rocket-pool/smartnode/shared/services/rewards/test"
15+
"github.com/rocket-pool/smartnode/shared/services/rewards/test/assets"
16+
"github.com/rocket-pool/smartnode/shared/services/state"
17+
"github.com/rocket-pool/smartnode/shared/utils/log"
18+
)
19+
20+
type v8Test struct {
21+
*testing.T
22+
rp *test.MockRocketPool
23+
bc *test.MockBeaconClient
24+
}
25+
26+
func (t *v8Test) saveArtifacts(result *GenerateTreeResult) {
27+
tmpDir, err := os.MkdirTemp("", fmt.Sprintf("artifacts-%s", t.Name()))
28+
t.failIf(err)
29+
rewardsLocalFile := LocalFile[IRewardsFile]{
30+
fullPath: filepath.Join(tmpDir, "rewards.json"),
31+
f: result.RewardsFile,
32+
}
33+
performanceLocalFile := LocalFile[IMinipoolPerformanceFile]{
34+
fullPath: filepath.Join(tmpDir, "minipool-performance.json"),
35+
f: result.MinipoolPerformanceFile,
36+
}
37+
_, err = rewardsLocalFile.Write()
38+
t.failIf(err)
39+
_, err = performanceLocalFile.Write()
40+
t.failIf(err)
41+
42+
t.Logf("wrote artifacts to %s\n", tmpDir)
43+
}
44+
45+
func newV8Test(t *testing.T) *v8Test {
46+
rp := test.NewMockRocketPool(t)
47+
out := &v8Test{
48+
T: t,
49+
rp: rp,
50+
bc: test.NewMockBeaconClient(t),
51+
}
52+
return out
53+
}
54+
55+
func (t *v8Test) failIf(err error) {
56+
if err != nil {
57+
t.Fatalf(err.Error())
58+
}
59+
}
60+
61+
func (t *v8Test) SetMinipoolPerformance(canonicalMinipoolPerformance IMinipoolPerformanceFile, networkState *state.NetworkState) {
62+
addresses := canonicalMinipoolPerformance.GetMinipoolAddresses()
63+
for _, address := range addresses {
64+
65+
// Get the minipool's performance
66+
perf, ok := canonicalMinipoolPerformance.GetSmoothingPoolPerformance(address)
67+
if !ok {
68+
t.Fatalf("Minipool %s not found in canonical minipool performance, despite being listed as present", address.Hex())
69+
}
70+
missedSlots := perf.GetMissingAttestationSlots()
71+
pubkey, err := perf.GetPubkey()
72+
73+
// Get the minipool's validator index
74+
validatorStatus := networkState.ValidatorDetails[pubkey]
75+
76+
if err != nil {
77+
t.Fatalf("Minipool %s pubkey could not be parsed: %s", address.Hex(), err.Error())
78+
}
79+
t.bc.SetMinipoolPerformance(validatorStatus.Index, missedSlots)
80+
}
81+
}
82+
83+
// TestV8Mainnet builds a tree using serialized state for a mainnet interval that used v8
84+
// and checks that the resulting artifacts match their canonical values.
85+
func TestV8Mainnet(tt *testing.T) {
86+
t := newV8Test(tt)
87+
88+
canonical, err := DeserializeRewardsFile(assets.GetMainnet20RewardsJSON())
89+
t.failIf(err)
90+
91+
canonicalPerformance, err := DeserializeMinipoolPerformanceFile(assets.GetMainnet20MinipoolPerformanceJSON())
92+
t.failIf(err)
93+
94+
state := assets.GetMainnet20RewardsState()
95+
t.Logf("pending rpl rewards: %s", state.NetworkDetails.PendingRPLRewards.String())
96+
97+
t.bc.SetState(state)
98+
99+
// Some interval info needed for mocks
100+
consensusStartBlock := canonical.GetConsensusStartBlock()
101+
executionStartBlock := canonical.GetExecutionStartBlock()
102+
consensusEndBlock := canonical.GetConsensusEndBlock()
103+
104+
// Create a new treeGeneratorImpl_v8
105+
logger := log.NewColorLogger(color.Faint)
106+
generator := newTreeGeneratorImpl_v8(
107+
&logger,
108+
t.Name(),
109+
canonical.GetIndex(),
110+
canonical.GetStartTime(),
111+
canonical.GetEndTime(),
112+
consensusEndBlock,
113+
&types.Header{
114+
Number: big.NewInt(int64(canonical.GetExecutionEndBlock())),
115+
Time: assets.Mainnet20ELHeaderTime,
116+
},
117+
canonical.GetIntervalsPassed(),
118+
state,
119+
)
120+
121+
// Load the mock up
122+
t.rp.SetRewardSnapshotEvent(assets.GetRewardSnapshotEventInterval19())
123+
t.bc.SetBeaconBlock(fmt.Sprint(consensusStartBlock-1), beacon.BeaconBlock{ExecutionBlockNumber: executionStartBlock - 1})
124+
t.bc.SetBeaconBlock(fmt.Sprint(consensusStartBlock), beacon.BeaconBlock{ExecutionBlockNumber: executionStartBlock})
125+
t.rp.SetHeaderByNumber(big.NewInt(int64(executionStartBlock)), &types.Header{Time: uint64(canonical.GetStartTime().Unix())})
126+
127+
// Set the critical duties slots
128+
t.bc.SetCriticalDutiesSlots(assets.GetMainnet20CriticalDutiesSlots())
129+
130+
// Set the minipool performance
131+
t.SetMinipoolPerformance(canonicalPerformance, state)
132+
133+
artifacts, err := generator.generateTree(
134+
t.rp,
135+
"mainnet",
136+
make([]common.Address, 0),
137+
t.bc,
138+
)
139+
t.failIf(err)
140+
141+
// Save the artifacts if verbose mode is enabled
142+
if testing.Verbose() {
143+
t.saveArtifacts(artifacts)
144+
}
145+
146+
t.Logf("merkle root: %s\n", artifacts.RewardsFile.GetMerkleRoot())
147+
if artifacts.RewardsFile.GetMerkleRoot() != canonical.GetMerkleRoot() {
148+
t.Fatalf("Merkle root does not match %s", canonical.GetMerkleRoot())
149+
} else {
150+
t.Logf("merkle root matches %s", canonical.GetMerkleRoot())
151+
}
152+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package assets
2+
3+
import (
4+
"bytes"
5+
"compress/gzip"
6+
_ "embed"
7+
"encoding/json"
8+
"io"
9+
"math/big"
10+
"time"
11+
12+
"github.com/ethereum/go-ethereum/common"
13+
"github.com/rocket-pool/rocketpool-go/rewards"
14+
"github.com/rocket-pool/smartnode/shared/services/state"
15+
)
16+
17+
const Mainnet20ELHeaderTime = 1710394571
18+
19+
//go:embed rp-rewards-mainnet-20.json.gz
20+
var mainnet20RewardsJSONGZ []byte
21+
var mainnet20RewardsJSON []byte
22+
23+
func GetMainnet20RewardsJSON() []byte {
24+
if mainnet20RewardsJSON != nil {
25+
return mainnet20RewardsJSON
26+
}
27+
28+
gz, err := gzip.NewReader(bytes.NewBuffer(mainnet20RewardsJSONGZ))
29+
if err != nil {
30+
panic(err)
31+
}
32+
defer gz.Close()
33+
mainnet20RewardsJSON, err = io.ReadAll(gz)
34+
if err != nil {
35+
panic(err)
36+
}
37+
return mainnet20RewardsJSON
38+
}
39+
40+
//go:embed rp-minipool-performance-mainnet-20.json.gz
41+
var Mainnet20MinipoolPerformanceJSONGZ []byte
42+
var Mainnet20MinipoolPerformanceJSON []byte
43+
44+
func GetMainnet20MinipoolPerformanceJSON() []byte {
45+
if Mainnet20MinipoolPerformanceJSON != nil {
46+
return Mainnet20MinipoolPerformanceJSON
47+
}
48+
49+
gz, err := gzip.NewReader(bytes.NewBuffer(Mainnet20MinipoolPerformanceJSONGZ))
50+
if err != nil {
51+
panic(err)
52+
}
53+
defer gz.Close()
54+
Mainnet20MinipoolPerformanceJSON, err = io.ReadAll(gz)
55+
if err != nil {
56+
panic(err)
57+
}
58+
return Mainnet20MinipoolPerformanceJSON
59+
}
60+
61+
//go:embed rp-network-state-mainnet-20.json.gz
62+
var Mainnet20NetworkStateJSONGZ []byte
63+
64+
var mainnet20RewardsState *state.NetworkState
65+
66+
func GetMainnet20RewardsState() *state.NetworkState {
67+
if mainnet20RewardsState != nil {
68+
return mainnet20RewardsState
69+
}
70+
71+
// GUnzip the embedded bytes
72+
gz, err := gzip.NewReader(bytes.NewBuffer(Mainnet20NetworkStateJSONGZ))
73+
if err != nil {
74+
panic(err)
75+
}
76+
defer gz.Close()
77+
78+
// Create a JSON decoder
79+
dec := json.NewDecoder(gz)
80+
81+
// Decode the JSON
82+
result := state.NetworkState{}
83+
err = dec.Decode(&result)
84+
if err != nil {
85+
panic(err)
86+
}
87+
88+
// Memoize the result
89+
mainnet20RewardsState = &result
90+
91+
return mainnet20RewardsState
92+
}
93+
94+
func GetRewardSnapshotEventInterval19() rewards.RewardsEvent {
95+
var rewardSnapshotEventInterval19 = rewards.RewardsEvent{
96+
Index: big.NewInt(19),
97+
ExecutionBlock: big.NewInt(19231284),
98+
ConsensusBlock: big.NewInt(8429279),
99+
MerkleRoot: common.HexToHash("0x35d1be64d49aa71dc5b5ea13dd6f91d8613c81aef2593796d6dee599cd228aea"),
100+
MerkleTreeCID: "bafybeiazkzsqe7molppbhbxg2khdgocrip36eoezroa7anbe53za7mxjpq",
101+
IntervalsPassed: big.NewInt(1),
102+
TreasuryRPL: big.NewInt(0), // Set below
103+
TrustedNodeRPL: []*big.Int{}, // XXX Not set, but probably not needed
104+
NodeRPL: []*big.Int{}, // XXX Not set, but probably not needed
105+
NodeETH: []*big.Int{}, // XXX Not set, but probably not needed
106+
UserETH: big.NewInt(0), // XXX Not set, but probably not needed
107+
IntervalStartTime: time.Unix(1705556139, 0),
108+
IntervalEndTime: time.Unix(1707975339, 0),
109+
SubmissionTime: time.Unix(1707976475, 0),
110+
}
111+
rewardSnapshotEventInterval19.TreasuryRPL.SetString("0x0000000000000000000000000000000000000000000000f0a1e7585cd758ffe2", 16)
112+
return rewardSnapshotEventInterval19
113+
}
114+
115+
//go:embed rp-network-critical-duties-mainnet-20.json.gz
116+
var mainnet20CriticalDutiesSlotsGZ []byte
117+
var mainnet20CriticalDutiesSlots *state.CriticalDutiesSlots
118+
119+
func GetMainnet20CriticalDutiesSlots() *state.CriticalDutiesSlots {
120+
if mainnet20CriticalDutiesSlots != nil {
121+
return mainnet20CriticalDutiesSlots
122+
}
123+
124+
jsonReader, err := gzip.NewReader(bytes.NewBuffer(mainnet20CriticalDutiesSlotsGZ))
125+
if err != nil {
126+
panic(err)
127+
}
128+
defer jsonReader.Close()
129+
130+
// Create a JSON decoder
131+
dec := json.NewDecoder(jsonReader)
132+
133+
// Decode the JSON
134+
result := state.CriticalDutiesSlots{}
135+
err = dec.Decode(&result)
136+
if err != nil {
137+
panic(err)
138+
}
139+
140+
mainnet20CriticalDutiesSlots = &result
141+
return mainnet20CriticalDutiesSlots
142+
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)