Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions bindings/megapool/beacon-state-verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,3 @@ func GetBeaconStateVerifierVersion(rp *rocketpool.RocketPool, opts *bind.CallOpt
}
return rocketpool.GetContractVersion(rp, *beaconStateVerifier.Address, opts)
}

func UsesProofBundles(rp *rocketpool.RocketPool, opts *bind.CallOpts) (bool, error) {
version, err := GetBeaconStateVerifierVersion(rp, opts)
if err != nil {
return false, err
}
return usesProofBundles(version), nil
}

func usesProofBundles(beaconStateVerifierVersion uint8) bool {
return beaconStateVerifierVersion >= 2
}
86 changes: 86 additions & 0 deletions bindings/megapool/challenge-bonds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package megapool

import (
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/rocket-pool/smartnode/bindings/logs"
"github.com/rocket-pool/smartnode/bindings/rocketpool"
)

// GetChallengeBondCandidates finds bonds proposed by this node and defeated
// challenges of either pool type. Defeat events do not identify the responder;
// callers must filter these candidates using getChallengeBondDetails.
func GetChallengeBondCandidates(rp *rocketpool.RocketPool, node common.Address, interval, fromBlock, toBlock *big.Int, opts *bind.CallOpts) ([]*big.Int, error) {
contract, err := getRocketNetworkParticipation(rp, opts)
if err != nil {
return nil, err
}
locked, ok := contract.ABI.Events["ChallengeBondLocked"]
if !ok {
return nil, fmt.Errorf("ChallengeBondLocked event missing from participation ABI")
}
megapoolDefeated, ok := contract.ABI.Events["MegapoolChallengeDefeated"]
if !ok {
return nil, fmt.Errorf("MegapoolChallengeDefeated event missing from participation ABI")
}
minipoolDefeated, ok := contract.ABI.Events["MinipoolChallengeDefeated"]
if !ok {
return nil, fmt.Errorf("MinipoolChallengeDefeated event missing from participation ABI")
}
fromBlock, toBlock, err = performanceChallengeScanRange(rp, fromBlock, toBlock, opts)
if err != nil || fromBlock == nil {
return nil, err
}
addresses, err := participationContractAddresses(rp, contract, interval, fromBlock, toBlock, opts)
if err != nil {
return nil, err
}
lockedLogs, err := logs.GetLogs(rp, addresses, [][]common.Hash{{locked.ID}, nil, {common.BytesToHash(node.Bytes())}}, interval, fromBlock, toBlock, nil)
if err != nil {
return nil, err
}
defeatedLogs, err := logs.GetLogs(rp, addresses, [][]common.Hash{{megapoolDefeated.ID, minipoolDefeated.ID}}, interval, fromBlock, toBlock, nil)
if err != nil {
return nil, err
}
ids := []*big.Int{}
seen := map[string]bool{}
add := func(id *big.Int) {
if !seen[id.String()] {
ids = append(ids, id)
seen[id.String()] = true
}
}
for _, entry := range lockedLogs {
if entry.Removed {
continue
}
if len(entry.Topics) != 3 || entry.Topics[0] != locked.ID {
return nil, fmt.Errorf("invalid ChallengeBondLocked event")
}
add(entry.Topics[1].Big())
}
for _, entry := range defeatedLogs {
if entry.Removed {
continue
}
if len(entry.Topics) != 1 {
return nil, fmt.Errorf("invalid challenge defeat event")
}
event := megapoolDefeated
if entry.Topics[0] == minipoolDefeated.ID {
event = minipoolDefeated
} else if entry.Topics[0] != megapoolDefeated.ID {
return nil, fmt.Errorf("unexpected challenge defeat event")
}
values, err := event.Inputs.NonIndexed().Unpack(entry.Data)
if err != nil || len(values) != 1 {
return nil, fmt.Errorf("invalid challenge defeat data: %v", err)
}
add(values[0].(*big.Int))
}
return ids, nil
}
175 changes: 175 additions & 0 deletions bindings/megapool/performance-challenges.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package megapool

import (
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/rocket-pool/smartnode/bindings/logs"
"github.com/rocket-pool/smartnode/bindings/rocketpool"
)

// PerformanceChallenge contains the data needed to reconstruct a challenge proof.
type PerformanceChallenge struct {
ChallengeId *big.Int
MegapoolAddress common.Address
NodeAddress common.Address
MinipoolAddresses []common.Address
ValidatorIds []uint32
StartEpoch uint64
Participation []*big.Int
}

type PerformanceChallengeStatus struct {
Proposer common.Address
Responder common.Address
BondAmount *big.Int
ResponseDeadline *big.Int
BondSettled bool
Responded bool
Finalised bool
}

func (s PerformanceChallengeStatus) CanReleaseBond(timestamp uint64) bool {
return !s.BondSettled && !s.Responded && s.ResponseDeadline.Cmp(new(big.Int).SetUint64(timestamp)) < 0
}

func (s PerformanceChallengeStatus) CanClaimReward(caller common.Address) bool {
return !s.BondSettled && s.Responded && s.Responder == caller
}

// GetMegapoolPerformanceChallenges reads challenge events, including events from
// previous deployments of rocketNetworkParticipation. A nil fromBlock scans
// from the first challenge, so overdue unresolved challenges are included.
func GetMegapoolPerformanceChallenges(rp *rocketpool.RocketPool, address common.Address, interval, fromBlock, toBlock *big.Int, opts *bind.CallOpts) ([]PerformanceChallenge, error) {
contract, err := getRocketNetworkParticipation(rp, opts)
if err != nil {
return nil, err
}
event, ok := contract.ABI.Events["MegapoolChallenged"]
if !ok {
return nil, fmt.Errorf("MegapoolChallenged event not found in rocketNetworkParticipation ABI")
}
fromBlock, toBlock, err = performanceChallengeScanRange(rp, fromBlock, toBlock, opts)
if err != nil || fromBlock == nil {
return nil, err
}
addresses, err := participationContractAddresses(rp, contract, interval, fromBlock, toBlock, opts)
if err != nil {
return nil, err
}

entries, err := logs.GetLogs(rp, addresses, [][]common.Hash{
{event.ID}, {common.BytesToHash(address.Bytes())},
}, interval, fromBlock, toBlock, nil)
if err != nil {
return nil, err
}
challenges := make([]PerformanceChallenge, 0, len(entries))
for _, entry := range entries {
if entry.Removed {
continue
}
challenge, err := decodePerformanceChallenge(event, entry)
if err != nil {
return nil, err
}
challenges = append(challenges, challenge)
}
return challenges, nil
}

func decodePerformanceChallenge(event abi.Event, entry types.Log) (PerformanceChallenge, error) {
if len(entry.Topics) != 3 || entry.Topics[0] != event.ID {
return PerformanceChallenge{}, fmt.Errorf("invalid MegapoolChallenged event topics")
}
values, err := event.Inputs.NonIndexed().Unpack(entry.Data)
if err != nil {
return PerformanceChallenge{}, fmt.Errorf("error unpacking MegapoolChallenged: %w", err)
}
if len(values) != 4 {
return PerformanceChallenge{}, fmt.Errorf("invalid MegapoolChallenged event data")
}
ids, idsOK := values[0].([]uint32)
start, startOK := values[1].(uint64)
participation, participationOK := values[3].([]*big.Int)
if !idsOK || !startOK || !participationOK || len(ids) == 0 || len(participation) == 0 {
return PerformanceChallenge{}, fmt.Errorf("invalid MegapoolChallenged validator list or participation bitmap")
}
return PerformanceChallenge{
ChallengeId: entry.Topics[2].Big(), MegapoolAddress: common.BytesToAddress(entry.Topics[1].Bytes()),
ValidatorIds: ids, StartEpoch: start, Participation: participation,
}, nil
}

// GetPerformanceChallengeStatus reads the snapshotted deadline and the two
// terminal flags. Bond settlement is independent of challenge finalisation.
func GetPerformanceChallengeStatus(rp *rocketpool.RocketPool, challengeId *big.Int, opts *bind.CallOpts) (PerformanceChallengeStatus, error) {
contract, err := getRocketNetworkParticipation(rp, opts)
if err != nil {
return PerformanceChallengeStatus{}, err
}
var values []interface{}
if err := contract.Contract.Call(opts, &values, "getChallengeBondDetails", challengeId); err != nil {
return PerformanceChallengeStatus{}, fmt.Errorf("error getting challenge %s bond details: %w", challengeId, err)
}
if len(values) != 5 {
return PerformanceChallengeStatus{}, fmt.Errorf("invalid challenge bond details")
}
status := PerformanceChallengeStatus{
Proposer: values[0].(common.Address), Responder: values[1].(common.Address),
BondAmount: values[2].(*big.Int), ResponseDeadline: values[3].(*big.Int), BondSettled: values[4].(bool),
}
for key, target := range map[string]*bool{
"participation.challenge.responded": &status.Responded,
"participation.challenge.finalised": &status.Finalised,
} {
// Solidity uses abi.encodePacked(string, uint256) for these storage keys.
hash := crypto.Keccak256Hash([]byte(key), common.LeftPadBytes(challengeId.Bytes(), 32))
value, err := rp.RocketStorage.GetBool(opts, hash)
if err != nil {
return PerformanceChallengeStatus{}, fmt.Errorf("error getting challenge %s status: %w", challengeId, err)
}
*target = value
}
return status, nil
}

// participationContractAddresses includes upgraded implementations without rescanning
// upgrade history outside the requested event range.
func participationContractAddresses(rp *rocketpool.RocketPool, contract *rocketpool.Contract, interval, fromBlock, toBlock *big.Int, opts *bind.CallOpts) ([]common.Address, error) {
// Include every address active within this range. Each upgrade identifies
// the old address; the current contract supplies the last address. Limiting
// upgrade discovery to the same range keeps subsequent scans incremental.
upgrade, err := rp.GetContract("rocketDAONodeTrustedUpgrade", opts)
if err != nil {
return nil, err
}
upgradeEvent, ok := upgrade.ABI.Events["ContractUpgraded"]
if !ok {
return nil, fmt.Errorf("ContractUpgraded event not found in rocketDAONodeTrustedUpgrade ABI")
}
upgrades, err := logs.GetLogs(rp, []common.Address{*upgrade.Address}, [][]common.Hash{
{upgradeEvent.ID}, {crypto.Keccak256Hash([]byte("rocketNetworkParticipation"))},
}, interval, fromBlock, toBlock, nil)
if err != nil {
return nil, err
}
addresses := []common.Address{*contract.Address}
seen := map[common.Address]bool{*contract.Address: true}
for _, entry := range upgrades {
if len(entry.Topics) != 4 {
return nil, fmt.Errorf("invalid ContractUpgraded event topics")
}
old := common.BytesToAddress(entry.Topics[2].Bytes())
if !seen[old] {
addresses = append(addresses, old)
seen[old] = true
}
}
return addresses, nil
}
Loading
Loading