-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Deterministic L1 starting block selection (Config in advance) #342
base: celo-rebase-12
Are you sure you want to change the base?
Changes from 20 commits
60f36c4
e0ab6c3
8d6a0d4
12b9558
b7ebbab
b4c7863
4a08a3c
7d4569d
dde8dce
9264244
c047c78
b311c43
cfba705
c2daa1d
702e087
3591c74
a9897b3
d56f9ed
402c2f1
ec03587
c027dab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,259 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/ethereum-optimism/optimism/op-service/eth" | ||
"github.com/ethereum/go-ethereum" | ||
"github.com/ethereum/go-ethereum/common" | ||
) | ||
|
||
const ( | ||
// See - https://eth2book.info/capella/part3/containers/state/ | ||
beaconChainGenesisTimeSeconds = 1606824000 | ||
beaconChainSlotDurationSeconds = 12 | ||
beaconSlotsPerEpoch = 32 | ||
) | ||
|
||
type beaconClient struct { | ||
cl *http.Client | ||
// A beaconchain RPC API endpoint. | ||
beaconRPC string | ||
// A beaconcha.in api endpoint. | ||
beaconchainURL string | ||
} | ||
|
||
func NewBeaconClient(beaconRPC string, beaconchainURL string) *beaconClient { | ||
return &beaconClient{ | ||
beaconRPC: beaconRPC, | ||
beaconchainURL: beaconchainURL, | ||
cl: &http.Client{}, | ||
} | ||
} | ||
|
||
// MostRecentFinalizedL1BlockAtTime returns the hash of the most recent | ||
// finalized L1 block at the given point in time. It finds the epoch within which the | ||
// given time falls and then looks back from there to find the first finalized | ||
// block. | ||
func (c *beaconClient) MostRecentFinalizedL1BlockAtTime(ctx context.Context, pointInTime uint64) (common.Hash, error) { | ||
// Find the epoch starting at or before the L2 start time. | ||
epochNumber := ContainingSlot(pointInTime) / beaconSlotsPerEpoch | ||
|
||
// This epoch is guaranteed to not be complete at L2 start time (if the L2 start time falls in the last | ||
// second of the epoch the epoch is still not complete, and if it was we'd be selecting the next epoch) | ||
// The previous epoch is the most recent completed epoch. | ||
// The one prior to that is the most recent justified epoch. | ||
// And the first block of the justified epoch (the epoch boundary block) will be finalized. | ||
// This is assuming that there was at least 2/3 participation in the completed and justified epochs. | ||
var epoch *Epoch | ||
var err error | ||
names := [2]string{"completed", "justified"} | ||
|
||
// Check the most recent completed and justified epochs had a participation rate of at least 0.67. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This and following steps are dependent on that the point-in-time has actually passed (in wall-time and on-chain in terms of prior epoch justifications). I think it would make sense to include the waiting logic here instead, since the function basically doesn't do anything when called before that time anyways. Additionally, you could make the point in time arg optional, and if it is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated to wait in this method, makes sense thanks! |
||
for i := uint64(1); i <= 2; i++ { | ||
epoch, err = c.Epoch(ctx, epochNumber-i) | ||
if err != nil { | ||
return common.Hash{}, fmt.Errorf("error fetching epoch %d: %w", epochNumber-i, err) | ||
} | ||
if epoch.Data.Globalparticipationrate < 0.67 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would have preferred to use the BeaconAPI for this, but it seems like you would have to process the BLS signatures and the graph of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, I think I can use the |
||
return common.Hash{}, fmt.Errorf("most recent %s epoch before the L2 start time (%d) has less than 0.67 participation rate (%.2f)", names[i-1], pointInTime, epoch.Data.Globalparticipationrate) | ||
} | ||
} | ||
// Calculate the first slot of the most recent justified epoch. | ||
mostRecentFinalizedSlot := (epochNumber - 2) * beaconSlotsPerEpoch | ||
|
||
// Find the most recent actual finalized block, slots can be empty so we | ||
// search back if we encounter empty slots. We check up to 10 slots, if they | ||
// are all empty something serious is wrong with the L1 so we abort. | ||
var beaconBlock *BeaconBlock | ||
for i := range uint64(10) { | ||
beaconBlock, err = c.BeaconBlock(ctx, mostRecentFinalizedSlot-i) | ||
if errors.Is(err, ethereum.NotFound) { | ||
// If there is not block for this slot then skip to the next. | ||
continue | ||
} | ||
if err != nil { | ||
return common.Hash{}, fmt.Errorf("error fetching beacon block at slot %d: %w", mostRecentFinalizedSlot, err) | ||
} | ||
if !beaconBlock.Finalized { | ||
return common.Hash{}, fmt.Errorf("expecting beacon block at slot %d to be finalized", mostRecentFinalizedSlot) | ||
} | ||
break // We found a good block. | ||
} | ||
if beaconBlock == nil { | ||
return common.Hash{}, fmt.Errorf("failed to find finalized block searching up to 10 slots back from the most recent finalized slot (%d) at the L2 fork time (%d)", mostRecentFinalizedSlot, pointInTime) | ||
} | ||
return common.HexToHash(beaconBlock.Data.Message.Body.ExecutionPayload.BlockHash), nil | ||
} | ||
|
||
// Epoch gets the requested epoch from the beaconcha.in api. | ||
func (c *beaconClient) Epoch(ctx context.Context, num uint64) (epoch *Epoch, err error) { | ||
piersy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/v1/epoch/%d", c.beaconchainURL, num), nil) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to create request to get epoch: %w", err) | ||
} | ||
resp, err := c.cl.Do(req) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to fetch epoch: %w", err) | ||
} | ||
defer func() { | ||
err = errors.Join(err, resp.Body.Close()) | ||
}() | ||
if resp.StatusCode != http.StatusOK { | ||
return nil, fmt.Errorf("failed to fetch epoch, http status %d", resp.StatusCode) | ||
} | ||
epoch = &Epoch{} | ||
if err := json.NewDecoder(resp.Body).Decode(epoch); err != nil { | ||
return nil, fmt.Errorf("failed to decode epoch: %w", err) | ||
} | ||
return epoch, nil | ||
} | ||
|
||
// BeaconBlock gets the beacon block from the beacon rpc api. | ||
func (c *beaconClient) BeaconBlock(ctx context.Context, slot uint64) (block *BeaconBlock, err error) { | ||
piersy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/eth/v2/beacon/blocks/%d", c.beaconRPC, slot), nil) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to create request to get beacon block: %w", err) | ||
} | ||
resp, err := c.cl.Do(req) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to fetch beacon block: %w", err) | ||
} | ||
defer func() { | ||
err = errors.Join(err, resp.Body.Close()) | ||
}() | ||
if resp.StatusCode != http.StatusOK { | ||
if resp.StatusCode == http.StatusNotFound { | ||
return nil, fmt.Errorf("failed to fetch beacon block at slot %d: %w", slot, ethereum.NotFound) | ||
} | ||
return nil, fmt.Errorf("failed to fetch beacon block at slot %d, http status %d", slot, resp.StatusCode) | ||
} | ||
if err := json.NewDecoder(resp.Body).Decode(&block); err != nil { | ||
return nil, fmt.Errorf("failed to decode beacon block: %w", err) | ||
} | ||
return block, nil | ||
} | ||
|
||
// EpochStartTime returns the start time of an epoch. | ||
func EpochStartTime(epoch uint64) uint64 { | ||
return beaconChainGenesisTimeSeconds + (epoch * beaconSlotsPerEpoch * beaconChainSlotDurationSeconds) | ||
} | ||
|
||
// ContainingEpoch returns the number of the epoch whithin which the given time falls. | ||
func ContainingEpoch(unixTime uint64) uint64 { | ||
return ContainingSlot(unixTime) / beaconSlotsPerEpoch | ||
} | ||
|
||
// ContainingSlot returns the slot within which the given time falls. | ||
func ContainingSlot(unixTime uint64) uint64 { | ||
// Get the slot at or before the given time. | ||
// Slot = (start - genesis) / slotDuration | ||
return (unixTime - beaconChainGenesisTimeSeconds) / beaconChainSlotDurationSeconds | ||
} | ||
|
||
type BeaconBlock struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there some lib for that kind of stuff? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found these two options:
But they both just provide clients for the beacon-rpc, so I'd still need to write a client for the beaconcha.in requests, it didn't seem worth it to import all that stuff for one method. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could use something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds interesting, I will have a look, where is the swagger file? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://ethereum.github.io/beacon-APIs/releases/v3.0.0/beacon-node-oapi.json The beacon-api specs is basically just a render of that |
||
Version string `json:"version"` | ||
ExecutionOptimistic bool `json:"execution_optimistic"` | ||
Finalized bool `json:"finalized"` | ||
Data struct { | ||
Message struct { | ||
Slot eth.Uint64String `json:"slot"` | ||
ProposerIndex string `json:"proposer_index"` | ||
ParentRoot string `json:"parent_root"` | ||
StateRoot string `json:"state_root"` | ||
Body struct { | ||
RandaoReveal string `json:"randao_reveal"` | ||
Eth1Data struct { | ||
DepositRoot string `json:"deposit_root"` | ||
DepositCount string `json:"deposit_count"` | ||
BlockHash string `json:"block_hash"` | ||
} `json:"eth1_data"` | ||
Graffiti string `json:"graffiti"` | ||
ProposerSlashings []interface{} `json:"proposer_slashings"` | ||
AttesterSlashings []interface{} `json:"attester_slashings"` | ||
Attestations []struct { | ||
AggregationBits string `json:"aggregation_bits"` | ||
Data struct { | ||
Slot string `json:"slot"` | ||
Index string `json:"index"` | ||
BeaconBlockRoot string `json:"beacon_block_root"` | ||
Source struct { | ||
Epoch string `json:"epoch"` | ||
Root string `json:"root"` | ||
} `json:"source"` | ||
Target struct { | ||
Epoch string `json:"epoch"` | ||
Root string `json:"root"` | ||
} `json:"target"` | ||
} `json:"data"` | ||
Signature string `json:"signature"` | ||
} `json:"attestations"` | ||
Deposits []interface{} `json:"deposits"` | ||
VoluntaryExits []interface{} `json:"voluntary_exits"` | ||
SyncAggregate struct { | ||
SyncCommitteeBits string `json:"sync_committee_bits"` | ||
SyncCommitteeSignature string `json:"sync_committee_signature"` | ||
} `json:"sync_aggregate"` | ||
ExecutionPayload struct { | ||
ParentHash string `json:"parent_hash"` | ||
FeeRecipient string `json:"fee_recipient"` | ||
StateRoot string `json:"state_root"` | ||
ReceiptsRoot string `json:"receipts_root"` | ||
LogsBloom string `json:"logs_bloom"` | ||
PrevRandao string `json:"prev_randao"` | ||
BlockNumber eth.Uint64String `json:"block_number"` | ||
GasLimit string `json:"gas_limit"` | ||
GasUsed string `json:"gas_used"` | ||
Timestamp eth.Uint64String `json:"timestamp"` | ||
ExtraData string `json:"extra_data"` | ||
BaseFeePerGas string `json:"base_fee_per_gas"` | ||
BlockHash string `json:"block_hash"` | ||
Transactions []string `json:"transactions"` | ||
Withdrawals []struct { | ||
Index string `json:"index"` | ||
ValidatorIndex string `json:"validator_index"` | ||
Address string `json:"address"` | ||
Amount string `json:"amount"` | ||
} `json:"withdrawals"` | ||
BlobGasUsed string `json:"blob_gas_used"` | ||
ExcessBlobGas string `json:"excess_blob_gas"` | ||
} `json:"execution_payload"` | ||
BlsToExecutionChanges []interface{} `json:"bls_to_execution_changes"` | ||
BlobKzgCommitments []string `json:"blob_kzg_commitments"` | ||
} `json:"body"` | ||
} `json:"message"` | ||
Signature string `json:"signature"` | ||
} `json:"data"` | ||
} | ||
|
||
type Epoch struct { | ||
Status string `json:"status"` | ||
Data struct { | ||
Attestationscount int `json:"attestationscount"` | ||
Attesterslashingscount int `json:"attesterslashingscount"` | ||
Averagevalidatorbalance int64 `json:"averagevalidatorbalance"` | ||
Blockscount int `json:"blockscount"` | ||
Depositscount int `json:"depositscount"` | ||
Eligibleether int64 `json:"eligibleether"` | ||
Epoch int `json:"epoch"` | ||
Finalized bool `json:"finalized"` | ||
Globalparticipationrate float64 `json:"globalparticipationrate"` | ||
Missedblocks int `json:"missedblocks"` | ||
Orphanedblocks int `json:"orphanedblocks"` | ||
Proposedblocks int `json:"proposedblocks"` | ||
Proposerslashingscount int `json:"proposerslashingscount"` | ||
RewardsExported bool `json:"rewards_exported"` | ||
Scheduledblocks int `json:"scheduledblocks"` | ||
Totalvalidatorbalance int64 `json:"totalvalidatorbalance"` | ||
Ts time.Time `json:"ts"` | ||
Validatorscount int `json:"validatorscount"` | ||
Voluntaryexitscount int `json:"voluntaryexitscount"` | ||
Votedether int64 `json:"votedether"` | ||
Withdrawalcount int `json:"withdrawalcount"` | ||
} `json:"data"` | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
"time" | ||
|
||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestFinalizedL1BlockSelection(t *testing.T) { | ||
bc := NewBeaconClient("https://eth-beacon-chain.drpc.org/rest", "https://beaconcha.in/api") | ||
var targetTime uint64 = 1740759647 // this is the timestamp of slot 11161302 https://beaconcha.in/slot/11161302 | ||
var expectedSlot uint64 = 11161216 // this is the beginning slot of the epoch 2 before https://beaconcha.in/slot/11161216 | ||
expectedL1BlockHash := common.HexToHash("0x6c71e110fc83faa393017681ab97b26621cda68d12e31d24917e210d169d7be5") | ||
|
||
// We expect the same block to be chosen regardless of which slot in an | ||
// epoch the target time falls in. The only thing that would change the | ||
// chosen block would be if the target time fell in a different epoch. | ||
|
||
t.Run("MidEpochSlot", func(t *testing.T) { | ||
checkFinalizedL1BlockSelection(t, bc, targetTime, expectedSlot, expectedL1BlockHash) | ||
}) | ||
|
||
t.Run("MidEpochMidSlot", func(t *testing.T) { | ||
checkFinalizedL1BlockSelection(t, bc, targetTime+beaconChainSlotDurationSeconds/2, expectedSlot, expectedL1BlockHash) | ||
}) | ||
|
||
epochFirstSlotTime := EpochStartTime(ContainingEpoch(targetTime)) | ||
t.Run("StartEpochSlot", func(t *testing.T) { | ||
checkFinalizedL1BlockSelection(t, bc, epochFirstSlotTime, expectedSlot, expectedL1BlockHash) | ||
}) | ||
|
||
epochLastSlotTime := epochFirstSlotTime + beaconChainSlotDurationSeconds*(beaconSlotsPerEpoch-1) | ||
t.Run("EndEpochSlot", func(t *testing.T) { | ||
checkFinalizedL1BlockSelection(t, bc, epochLastSlotTime, expectedSlot, expectedL1BlockHash) | ||
}) | ||
|
||
// Execution payload block hash retrieved here - https://beaconcha.in/slot/11161184 | ||
expectedL1BlockHashPrevEpoch := common.HexToHash("0xca6237f41190e1adfa52ba20fadb03ef14a7f57c516806edf7660f301b08de36") | ||
t.Run("PriorEpochEndSlot", func(t *testing.T) { | ||
checkFinalizedL1BlockSelection(t, bc, epochFirstSlotTime-1, expectedSlot-beaconSlotsPerEpoch, expectedL1BlockHashPrevEpoch) | ||
}) | ||
|
||
// Execution payload block hash retrieved here - https://beaconcha.in/slot/11161248 | ||
expectedL1BlockHashNextEpoch := common.HexToHash("0x7892cbc14d57ab7036a722306bcae1fc07a202fc5907a227cce539b5f1ca41b3") | ||
t.Run("NextEpochStartSlot", func(t *testing.T) { | ||
checkFinalizedL1BlockSelection(t, bc, epochLastSlotTime+beaconChainSlotDurationSeconds, expectedSlot+beaconSlotsPerEpoch, expectedL1BlockHashNextEpoch) | ||
}) | ||
} | ||
|
||
func checkFinalizedL1BlockSelection(t *testing.T, bc *beaconClient, targetTime, expectedSlot uint64, expectedBlockHash common.Hash) { | ||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) | ||
t.Cleanup(cancel) | ||
calculatedHash, err := bc.MostRecentFinalizedL1BlockAtTime(ctx, targetTime) | ||
require.NoError(t, err) | ||
assert.Equal(t, expectedBlockHash, calculatedHash) | ||
// Double check that we are targeting the correct slot | ||
block, err := bc.BeaconBlock(ctx, expectedSlot) | ||
require.NoError(t, err) | ||
assert.Equal(t, expectedBlockHash.String(), block.Data.Message.Body.ExecutionPayload.BlockHash) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you renamed the arg, but the comment still references the old name