mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-02-12 22:15:07 -05:00
Compare commits
60 Commits
develop
...
feat/updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b8a6baad3 | ||
|
|
fd07e59c0a | ||
|
|
d66d25e7ad | ||
|
|
89ab513183 | ||
|
|
1c6110b6e8 | ||
|
|
a82edc7fbc | ||
|
|
39bb8d2929 | ||
|
|
80e0227bac | ||
|
|
387cfcd442 | ||
|
|
c70e51b445 | ||
|
|
a1a8a86341 | ||
|
|
71b1610331 | ||
|
|
814abab4b5 | ||
|
|
d0d6bab8a0 | ||
|
|
3f6c01fc3b | ||
|
|
c5914ea4d9 | ||
|
|
08d143bd2c | ||
|
|
b36bc1fe17 | ||
|
|
a329a77037 | ||
|
|
bfd9ff8651 | ||
|
|
79301d4db6 | ||
|
|
fbfea6f753 | ||
|
|
562ef25527 | ||
|
|
488971f989 | ||
|
|
b129eaaeb8 | ||
|
|
90302adbd2 | ||
|
|
a6262ba07b | ||
|
|
7d5c8d6964 | ||
|
|
38cae3f8de | ||
|
|
a5e6d0a3ac | ||
|
|
c4a308e598 | ||
|
|
ba720c1b4b | ||
|
|
5a3f45f91f | ||
|
|
29010edcd1 | ||
|
|
045c29ccce | ||
|
|
0d80bbe44f | ||
|
|
14f13ed902 | ||
|
|
308199c2e7 | ||
|
|
7a04c6b645 | ||
|
|
93cda45e18 | ||
|
|
1b4265ef3f | ||
|
|
90d1716fd7 | ||
|
|
c4f1a9ac4f | ||
|
|
73d948a710 | ||
|
|
5e2985d36b | ||
|
|
7ac3f3cb68 | ||
|
|
0a759c3d15 | ||
|
|
bf7ca00780 | ||
|
|
efcb98bcaa | ||
|
|
7b0de5ad0e | ||
|
|
2e15cd2068 | ||
|
|
2e5192e496 | ||
|
|
729c54a300 | ||
|
|
4556aa266a | ||
|
|
2f1cac217d | ||
|
|
6bfc779ea1 | ||
|
|
90481d6aa8 | ||
|
|
9f64007dc1 | ||
|
|
879ea624ec | ||
|
|
79841f451c |
8
.github/workflows/check-specrefs.yml
vendored
8
.github/workflows/check-specrefs.yml
vendored
@@ -12,11 +12,11 @@ jobs:
|
||||
- name: Check version consistency
|
||||
run: |
|
||||
WORKSPACE_VERSION=$(grep 'consensus_spec_version = ' WORKSPACE | sed 's/.*"\(.*\)"/\1/')
|
||||
ETHSPECIFY_VERSION=$(grep '^version:' .ethspecify.yml | sed 's/version: //')
|
||||
ETHSPECIFY_VERSION=$(grep '^version:' specrefs/.ethspecify.yml | sed 's/version: //')
|
||||
if [ "$WORKSPACE_VERSION" != "$ETHSPECIFY_VERSION" ]; then
|
||||
echo "Version mismatch between WORKSPACE and ethspecify"
|
||||
echo " WORKSPACE: $WORKSPACE_VERSION"
|
||||
echo " .ethspecify.yml: $ETHSPECIFY_VERSION"
|
||||
echo " specrefs/.ethspecify.yml: $ETHSPECIFY_VERSION"
|
||||
exit 1
|
||||
else
|
||||
echo "Versions match: $WORKSPACE_VERSION"
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
run: python3 -mpip install ethspecify
|
||||
|
||||
- name: Update spec references
|
||||
run: ethspecify
|
||||
run: ethspecify process --path=specrefs
|
||||
|
||||
- name: Check for differences
|
||||
run: |
|
||||
@@ -40,4 +40,4 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Check spec references
|
||||
run: ethspecify check
|
||||
run: ethspecify check --path=specrefs
|
||||
|
||||
10
WORKSPACE
10
WORKSPACE
@@ -273,16 +273,16 @@ filegroup(
|
||||
url = "https://github.com/ethereum/EIPs/archive/5480440fe51742ed23342b68cf106cefd427e39d.tar.gz",
|
||||
)
|
||||
|
||||
consensus_spec_version = "v1.7.0-alpha.2"
|
||||
consensus_spec_version = "v1.7.0-alpha.1"
|
||||
|
||||
load("@prysm//tools:download_spectests.bzl", "consensus_spec_tests")
|
||||
|
||||
consensus_spec_tests(
|
||||
name = "consensus_spec_tests",
|
||||
flavors = {
|
||||
"general": "sha256-iGQsGZ1cHah+2CSod9jC3kN8Ku4n6KO0hIwfINrn/po=",
|
||||
"minimal": "sha256-TgcYt8N8sXSttdHTGvOa+exUZ1zn1UzlAMz0V7i37xc=",
|
||||
"mainnet": "sha256-LnXyiLoJtrvEvbqLDSAAqpLMdN/lXv92SAgYG8fNjCs=",
|
||||
"general": "sha256-j5R3jA7Oo4OSDMTvpMuD+8RomaCByeFSwtfkq6fL0Zg=",
|
||||
"minimal": "sha256-tdTqByoyswOS4r6OxFmo70y2BP7w1TgEok+gf4cbxB0=",
|
||||
"mainnet": "sha256-5gB4dt6SnSDKzdBc06VedId3NkgvSYyv9n9FRxWKwYI=",
|
||||
},
|
||||
version = consensus_spec_version,
|
||||
)
|
||||
@@ -298,7 +298,7 @@ filegroup(
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
""",
|
||||
integrity = "sha256-Y/67Dg393PksZj5rTFNLntiJ6hNdB7Rxbu5gZE2gebY=",
|
||||
integrity = "sha256-J+43DrK1pF658kTXTwMS6zGf4KDjvas++m8w2a8swpg=",
|
||||
strip_prefix = "consensus-specs-" + consensus_spec_version[1:],
|
||||
url = "https://github.com/ethereum/consensus-specs/archive/refs/tags/%s.tar.gz" % consensus_spec_version,
|
||||
)
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
load("@prysm//tools/go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"fallback.go",
|
||||
"log.go",
|
||||
],
|
||||
importpath = "github.com/OffchainLabs/prysm/v7/api/fallback",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = ["@com_github_sirupsen_logrus//:go_default_library"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["fallback_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = ["//testing/assert:go_default_library"],
|
||||
)
|
||||
@@ -1,66 +0,0 @@
|
||||
package fallback
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// HostProvider is the subset of connection-provider methods that EnsureReady
|
||||
// needs. Both grpc.GrpcConnectionProvider and rest.RestConnectionProvider
|
||||
// satisfy this interface.
|
||||
type HostProvider interface {
|
||||
Hosts() []string
|
||||
CurrentHost() string
|
||||
SwitchHost(index int) error
|
||||
}
|
||||
|
||||
// ReadyChecker can report whether the current endpoint is ready.
|
||||
// iface.NodeClient satisfies this implicitly.
|
||||
type ReadyChecker interface {
|
||||
IsReady(ctx context.Context) bool
|
||||
}
|
||||
|
||||
// EnsureReady iterates through the configured hosts and returns true as soon as
|
||||
// one responds as ready. It starts from the provider's current host and wraps
|
||||
// around using modular arithmetic, performing failover when a host is not ready.
|
||||
func EnsureReady(ctx context.Context, provider HostProvider, checker ReadyChecker) bool {
|
||||
hosts := provider.Hosts()
|
||||
numHosts := len(hosts)
|
||||
startingHost := provider.CurrentHost()
|
||||
var attemptedHosts []string
|
||||
|
||||
// Find current index
|
||||
currentIdx := 0
|
||||
for i, h := range hosts {
|
||||
if h == startingHost {
|
||||
currentIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for i := range numHosts {
|
||||
if checker.IsReady(ctx) {
|
||||
if len(attemptedHosts) > 0 {
|
||||
log.WithFields(logrus.Fields{
|
||||
"previous": startingHost,
|
||||
"current": provider.CurrentHost(),
|
||||
"tried": attemptedHosts,
|
||||
}).Info("Switched to responsive beacon node")
|
||||
}
|
||||
return true
|
||||
}
|
||||
attemptedHosts = append(attemptedHosts, provider.CurrentHost())
|
||||
|
||||
// Try next host if not the last iteration
|
||||
if i < numHosts-1 {
|
||||
nextIdx := (currentIdx + i + 1) % numHosts
|
||||
if err := provider.SwitchHost(nextIdx); err != nil {
|
||||
log.WithError(err).Error("Failed to switch host")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.WithField("tried", attemptedHosts).Warn("No responsive beacon node found")
|
||||
return false
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package fallback
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/testing/assert"
|
||||
)
|
||||
|
||||
// mockHostProvider is a minimal HostProvider for unit tests.
|
||||
type mockHostProvider struct {
|
||||
hosts []string
|
||||
hostIndex int
|
||||
}
|
||||
|
||||
func (m *mockHostProvider) Hosts() []string { return m.hosts }
|
||||
func (m *mockHostProvider) CurrentHost() string {
|
||||
return m.hosts[m.hostIndex%len(m.hosts)]
|
||||
}
|
||||
func (m *mockHostProvider) SwitchHost(index int) error { m.hostIndex = index; return nil }
|
||||
|
||||
// mockReadyChecker records per-call IsReady results in sequence.
|
||||
type mockReadyChecker struct {
|
||||
results []bool
|
||||
idx int
|
||||
}
|
||||
|
||||
func (m *mockReadyChecker) IsReady(_ context.Context) bool {
|
||||
if m.idx >= len(m.results) {
|
||||
return false
|
||||
}
|
||||
r := m.results[m.idx]
|
||||
m.idx++
|
||||
return r
|
||||
}
|
||||
|
||||
func TestEnsureReady_SingleHostReady(t *testing.T) {
|
||||
provider := &mockHostProvider{hosts: []string{"http://host1:3500"}, hostIndex: 0}
|
||||
checker := &mockReadyChecker{results: []bool{true}}
|
||||
assert.Equal(t, true, EnsureReady(t.Context(), provider, checker))
|
||||
assert.Equal(t, 0, provider.hostIndex)
|
||||
}
|
||||
|
||||
func TestEnsureReady_SingleHostNotReady(t *testing.T) {
|
||||
provider := &mockHostProvider{hosts: []string{"http://host1:3500"}, hostIndex: 0}
|
||||
checker := &mockReadyChecker{results: []bool{false}}
|
||||
assert.Equal(t, false, EnsureReady(t.Context(), provider, checker))
|
||||
}
|
||||
|
||||
func TestEnsureReady_SingleHostError(t *testing.T) {
|
||||
provider := &mockHostProvider{hosts: []string{"http://host1:3500"}, hostIndex: 0}
|
||||
checker := &mockReadyChecker{results: []bool{false}}
|
||||
assert.Equal(t, false, EnsureReady(t.Context(), provider, checker))
|
||||
}
|
||||
|
||||
func TestEnsureReady_MultipleHostsFirstReady(t *testing.T) {
|
||||
provider := &mockHostProvider{
|
||||
hosts: []string{"http://host1:3500", "http://host2:3500"},
|
||||
hostIndex: 0,
|
||||
}
|
||||
checker := &mockReadyChecker{results: []bool{true}}
|
||||
assert.Equal(t, true, EnsureReady(t.Context(), provider, checker))
|
||||
assert.Equal(t, 0, provider.hostIndex)
|
||||
}
|
||||
|
||||
func TestEnsureReady_MultipleHostsFailoverToSecond(t *testing.T) {
|
||||
provider := &mockHostProvider{
|
||||
hosts: []string{"http://host1:3500", "http://host2:3500"},
|
||||
hostIndex: 0,
|
||||
}
|
||||
checker := &mockReadyChecker{results: []bool{false, true}}
|
||||
assert.Equal(t, true, EnsureReady(t.Context(), provider, checker))
|
||||
assert.Equal(t, 1, provider.hostIndex)
|
||||
}
|
||||
|
||||
func TestEnsureReady_MultipleHostsNoneReady(t *testing.T) {
|
||||
provider := &mockHostProvider{
|
||||
hosts: []string{"http://host1:3500", "http://host2:3500", "http://host3:3500"},
|
||||
hostIndex: 0,
|
||||
}
|
||||
checker := &mockReadyChecker{results: []bool{false, false, false}}
|
||||
assert.Equal(t, false, EnsureReady(t.Context(), provider, checker))
|
||||
}
|
||||
|
||||
func TestEnsureReady_WrapAroundFromNonZeroIndex(t *testing.T) {
|
||||
provider := &mockHostProvider{
|
||||
hosts: []string{"http://host0:3500", "http://host1:3500", "http://host2:3500"},
|
||||
hostIndex: 1,
|
||||
}
|
||||
// host1 (start) fails, host2 fails, host0 succeeds
|
||||
checker := &mockReadyChecker{results: []bool{false, false, true}}
|
||||
assert.Equal(t, true, EnsureReady(t.Context(), provider, checker))
|
||||
assert.Equal(t, 0, provider.hostIndex)
|
||||
}
|
||||
@@ -25,11 +25,6 @@ type GrpcConnectionProvider interface {
|
||||
// SwitchHost switches to the endpoint at the given index.
|
||||
// The new connection is created lazily on next CurrentConn() call.
|
||||
SwitchHost(index int) error
|
||||
// ConnectionCounter returns a monotonically increasing counter that increments
|
||||
// each time SwitchHost changes the active endpoint. This allows consumers to
|
||||
// detect connection changes even when the host string returns to a previous value
|
||||
// (e.g., host0 → host1 → host0).
|
||||
ConnectionCounter() uint64
|
||||
// Close closes the current connection.
|
||||
Close()
|
||||
}
|
||||
@@ -43,7 +38,6 @@ type grpcConnectionProvider struct {
|
||||
// Current connection state (protected by mutex)
|
||||
currentIndex uint64
|
||||
conn *grpc.ClientConn
|
||||
connCounter uint64
|
||||
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
@@ -144,7 +138,6 @@ func (p *grpcConnectionProvider) SwitchHost(index int) error {
|
||||
|
||||
p.conn = nil // Clear immediately - new connection created lazily
|
||||
p.currentIndex = uint64(index)
|
||||
p.connCounter++
|
||||
|
||||
// Close old connection asynchronously to avoid blocking the caller
|
||||
if oldConn != nil {
|
||||
@@ -162,12 +155,6 @@ func (p *grpcConnectionProvider) SwitchHost(index int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *grpcConnectionProvider) ConnectionCounter() uint64 {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
return p.connCounter
|
||||
}
|
||||
|
||||
func (p *grpcConnectionProvider) Close() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
@@ -4,24 +4,17 @@ import "google.golang.org/grpc"
|
||||
|
||||
// MockGrpcProvider implements GrpcConnectionProvider for testing.
|
||||
type MockGrpcProvider struct {
|
||||
MockConn *grpc.ClientConn
|
||||
MockHosts []string
|
||||
CurrentIndex int
|
||||
ConnCounter uint64
|
||||
MockConn *grpc.ClientConn
|
||||
MockHosts []string
|
||||
}
|
||||
|
||||
func (m *MockGrpcProvider) CurrentConn() *grpc.ClientConn { return m.MockConn }
|
||||
func (m *MockGrpcProvider) CurrentHost() string {
|
||||
if len(m.MockHosts) > 0 {
|
||||
return m.MockHosts[m.CurrentIndex]
|
||||
return m.MockHosts[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (m *MockGrpcProvider) Hosts() []string { return m.MockHosts }
|
||||
func (m *MockGrpcProvider) SwitchHost(idx int) error {
|
||||
m.CurrentIndex = idx
|
||||
m.ConnCounter++
|
||||
return nil
|
||||
}
|
||||
func (m *MockGrpcProvider) ConnectionCounter() uint64 { return m.ConnCounter }
|
||||
func (m *MockGrpcProvider) Close() {}
|
||||
func (m *MockGrpcProvider) Hosts() []string { return m.MockHosts }
|
||||
func (m *MockGrpcProvider) SwitchHost(int) error { return nil }
|
||||
func (m *MockGrpcProvider) Close() {}
|
||||
|
||||
@@ -9,13 +9,13 @@ import (
|
||||
// MockRestProvider implements RestConnectionProvider for testing.
|
||||
type MockRestProvider struct {
|
||||
MockClient *http.Client
|
||||
MockHandler Handler
|
||||
MockHandler RestHandler
|
||||
MockHosts []string
|
||||
HostIndex int
|
||||
}
|
||||
|
||||
func (m *MockRestProvider) HttpClient() *http.Client { return m.MockClient }
|
||||
func (m *MockRestProvider) Handler() Handler { return m.MockHandler }
|
||||
func (m *MockRestProvider) RestHandler() RestHandler { return m.MockHandler }
|
||||
func (m *MockRestProvider) CurrentHost() string {
|
||||
if len(m.MockHosts) > 0 {
|
||||
return m.MockHosts[m.HostIndex%len(m.MockHosts)]
|
||||
@@ -25,22 +25,25 @@ func (m *MockRestProvider) CurrentHost() string {
|
||||
func (m *MockRestProvider) Hosts() []string { return m.MockHosts }
|
||||
func (m *MockRestProvider) SwitchHost(index int) error { m.HostIndex = index; return nil }
|
||||
|
||||
// MockHandler implements Handler for testing.
|
||||
type MockHandler struct {
|
||||
MockHost string
|
||||
// MockRestHandler implements RestHandler for testing.
|
||||
type MockRestHandler struct {
|
||||
MockHost string
|
||||
MockClient *http.Client
|
||||
}
|
||||
|
||||
func (m *MockHandler) Get(_ context.Context, _ string, _ any) error { return nil }
|
||||
func (m *MockHandler) GetStatusCode(_ context.Context, _ string) (int, error) {
|
||||
func (m *MockRestHandler) Get(_ context.Context, _ string, _ any) error { return nil }
|
||||
func (m *MockRestHandler) GetStatusCode(_ context.Context, _ string) (int, error) {
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
func (m *MockHandler) GetSSZ(_ context.Context, _ string) ([]byte, http.Header, error) {
|
||||
func (m *MockRestHandler) GetSSZ(_ context.Context, _ string) ([]byte, http.Header, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
func (m *MockHandler) Post(_ context.Context, _ string, _ map[string]string, _ *bytes.Buffer, _ any) error {
|
||||
func (m *MockRestHandler) Post(_ context.Context, _ string, _ map[string]string, _ *bytes.Buffer, _ any) error {
|
||||
return nil
|
||||
}
|
||||
func (m *MockHandler) PostSSZ(_ context.Context, _ string, _ map[string]string, _ *bytes.Buffer) ([]byte, http.Header, error) {
|
||||
func (m *MockRestHandler) PostSSZ(_ context.Context, _ string, _ map[string]string, _ *bytes.Buffer) ([]byte, http.Header, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
func (m *MockHandler) Host() string { return m.MockHost }
|
||||
func (m *MockRestHandler) HttpClient() *http.Client { return m.MockClient }
|
||||
func (m *MockRestHandler) Host() string { return m.MockHost }
|
||||
func (m *MockRestHandler) SwitchHost(host string) { m.MockHost = host }
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
type RestConnectionProvider interface {
|
||||
// HttpClient returns the configured HTTP client with headers, timeout, and optional tracing.
|
||||
HttpClient() *http.Client
|
||||
// Handler returns the REST handler for making API requests.
|
||||
Handler() Handler
|
||||
// RestHandler returns the REST handler for making API requests.
|
||||
RestHandler() RestHandler
|
||||
// CurrentHost returns the current REST API endpoint URL.
|
||||
CurrentHost() string
|
||||
// Hosts returns all configured REST API endpoint URLs.
|
||||
@@ -54,7 +54,7 @@ func WithTracing() RestConnectionProviderOption {
|
||||
type restConnectionProvider struct {
|
||||
endpoints []string
|
||||
httpClient *http.Client
|
||||
restHandler *handler
|
||||
restHandler RestHandler
|
||||
currentIndex atomic.Uint64
|
||||
timeout time.Duration
|
||||
headers map[string][]string
|
||||
@@ -96,7 +96,7 @@ func NewRestConnectionProvider(endpoint string, opts ...RestConnectionProviderOp
|
||||
}
|
||||
|
||||
// Create the REST handler with the HTTP client and initial host
|
||||
p.restHandler = newHandler(*p.httpClient, endpoints[0])
|
||||
p.restHandler = newRestHandler(*p.httpClient, endpoints[0])
|
||||
|
||||
log.WithFields(logrus.Fields{
|
||||
"endpoints": endpoints,
|
||||
@@ -124,7 +124,7 @@ func (p *restConnectionProvider) HttpClient() *http.Client {
|
||||
return p.httpClient
|
||||
}
|
||||
|
||||
func (p *restConnectionProvider) Handler() Handler {
|
||||
func (p *restConnectionProvider) RestHandler() RestHandler {
|
||||
return p.restHandler
|
||||
}
|
||||
|
||||
|
||||
@@ -21,35 +21,32 @@ import (
|
||||
|
||||
type reqOption func(*http.Request)
|
||||
|
||||
// Handler defines the interface for making REST API requests.
|
||||
type Handler interface {
|
||||
// RestHandler defines the interface for making REST API requests.
|
||||
type RestHandler interface {
|
||||
Get(ctx context.Context, endpoint string, resp any) error
|
||||
GetStatusCode(ctx context.Context, endpoint string) (int, error)
|
||||
GetSSZ(ctx context.Context, endpoint string) ([]byte, http.Header, error)
|
||||
Post(ctx context.Context, endpoint string, headers map[string]string, data *bytes.Buffer, resp any) error
|
||||
PostSSZ(ctx context.Context, endpoint string, headers map[string]string, data *bytes.Buffer) ([]byte, http.Header, error)
|
||||
HttpClient() *http.Client
|
||||
Host() string
|
||||
SwitchHost(host string)
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
type restHandler struct {
|
||||
client http.Client
|
||||
host string
|
||||
reqOverrides []reqOption
|
||||
}
|
||||
|
||||
// newHandler returns a *handler for internal use within the rest package.
|
||||
func newHandler(client http.Client, host string) *handler {
|
||||
rh := &handler{
|
||||
client: client,
|
||||
host: host,
|
||||
}
|
||||
rh.appendAcceptOverride()
|
||||
return rh
|
||||
// newRestHandler returns a RestHandler (internal use)
|
||||
func newRestHandler(client http.Client, host string) RestHandler {
|
||||
return NewRestHandler(client, host)
|
||||
}
|
||||
|
||||
// NewHandler returns a Handler
|
||||
func NewHandler(client http.Client, host string) Handler {
|
||||
rh := &handler{
|
||||
// NewRestHandler returns a RestHandler
|
||||
func NewRestHandler(client http.Client, host string) RestHandler {
|
||||
rh := &restHandler{
|
||||
client: client,
|
||||
host: host,
|
||||
}
|
||||
@@ -60,7 +57,7 @@ func NewHandler(client http.Client, host string) Handler {
|
||||
// appendAcceptOverride enables the Accept header to be customized at runtime via an environment variable.
|
||||
// This is specified as an env var because it is a niche option that prysm may use for performance testing or debugging
|
||||
// bug which users are unlikely to need. Using an env var keeps the set of user-facing flags cleaner.
|
||||
func (c *handler) appendAcceptOverride() {
|
||||
func (c *restHandler) appendAcceptOverride() {
|
||||
if accept := os.Getenv(params.EnvNameOverrideAccept); accept != "" {
|
||||
c.reqOverrides = append(c.reqOverrides, func(req *http.Request) {
|
||||
req.Header.Set("Accept", accept)
|
||||
@@ -69,18 +66,18 @@ func (c *handler) appendAcceptOverride() {
|
||||
}
|
||||
|
||||
// HttpClient returns the underlying HTTP client of the handler
|
||||
func (c *handler) HttpClient() *http.Client {
|
||||
func (c *restHandler) HttpClient() *http.Client {
|
||||
return &c.client
|
||||
}
|
||||
|
||||
// Host returns the underlying HTTP host
|
||||
func (c *handler) Host() string {
|
||||
func (c *restHandler) Host() string {
|
||||
return c.host
|
||||
}
|
||||
|
||||
// Get sends a GET request and decodes the response body as a JSON object into the passed in object.
|
||||
// If an HTTP error is returned, the body is decoded as a DefaultJsonError JSON object and returned as the first return value.
|
||||
func (c *handler) Get(ctx context.Context, endpoint string, resp any) error {
|
||||
func (c *restHandler) Get(ctx context.Context, endpoint string, resp any) error {
|
||||
url := c.host + endpoint
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
@@ -103,7 +100,7 @@ func (c *handler) Get(ctx context.Context, endpoint string, resp any) error {
|
||||
// GetStatusCode sends a GET request and returns only the HTTP status code.
|
||||
// This is useful for endpoints like /eth/v1/node/health that communicate status via HTTP codes
|
||||
// (200 = ready, 206 = syncing, 503 = unavailable) rather than response bodies.
|
||||
func (c *handler) GetStatusCode(ctx context.Context, endpoint string) (int, error) {
|
||||
func (c *restHandler) GetStatusCode(ctx context.Context, endpoint string) (int, error) {
|
||||
url := c.host + endpoint
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
@@ -122,7 +119,7 @@ func (c *handler) GetStatusCode(ctx context.Context, endpoint string) (int, erro
|
||||
return httpResp.StatusCode, nil
|
||||
}
|
||||
|
||||
func (c *handler) GetSSZ(ctx context.Context, endpoint string) ([]byte, http.Header, error) {
|
||||
func (c *restHandler) GetSSZ(ctx context.Context, endpoint string) ([]byte, http.Header, error) {
|
||||
url := c.host + endpoint
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
@@ -177,7 +174,7 @@ func (c *handler) GetSSZ(ctx context.Context, endpoint string) ([]byte, http.Hea
|
||||
|
||||
// Post sends a POST request and decodes the response body as a JSON object into the passed in object.
|
||||
// If an HTTP error is returned, the body is decoded as a DefaultJsonError JSON object and returned as the first return value.
|
||||
func (c *handler) Post(
|
||||
func (c *restHandler) Post(
|
||||
ctx context.Context,
|
||||
apiEndpoint string,
|
||||
headers map[string]string,
|
||||
@@ -213,7 +210,7 @@ func (c *handler) Post(
|
||||
}
|
||||
|
||||
// PostSSZ sends a POST request and prefers an SSZ (application/octet-stream) response body.
|
||||
func (c *handler) PostSSZ(
|
||||
func (c *restHandler) PostSSZ(
|
||||
ctx context.Context,
|
||||
apiEndpoint string,
|
||||
headers map[string]string,
|
||||
@@ -314,6 +311,6 @@ func decodeResp(httpResp *http.Response, resp any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *handler) SwitchHost(host string) {
|
||||
func (c *restHandler) SwitchHost(host string) {
|
||||
c.host = host
|
||||
}
|
||||
|
||||
@@ -509,17 +509,17 @@ func (s *SignedBlindedBeaconBlockFulu) SigString() string {
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type ExecutionPayloadBid struct {
|
||||
ParentBlockHash string `json:"parent_block_hash"`
|
||||
ParentBlockRoot string `json:"parent_block_root"`
|
||||
BlockHash string `json:"block_hash"`
|
||||
PrevRandao string `json:"prev_randao"`
|
||||
FeeRecipient string `json:"fee_recipient"`
|
||||
GasLimit string `json:"gas_limit"`
|
||||
BuilderIndex string `json:"builder_index"`
|
||||
Slot string `json:"slot"`
|
||||
Value string `json:"value"`
|
||||
ExecutionPayment string `json:"execution_payment"`
|
||||
BlobKzgCommitments []string `json:"blob_kzg_commitments"`
|
||||
ParentBlockHash string `json:"parent_block_hash"`
|
||||
ParentBlockRoot string `json:"parent_block_root"`
|
||||
BlockHash string `json:"block_hash"`
|
||||
PrevRandao string `json:"prev_randao"`
|
||||
FeeRecipient string `json:"fee_recipient"`
|
||||
GasLimit string `json:"gas_limit"`
|
||||
BuilderIndex string `json:"builder_index"`
|
||||
Slot string `json:"slot"`
|
||||
Value string `json:"value"`
|
||||
ExecutionPayment string `json:"execution_payment"`
|
||||
BlobKzgCommitmentsRoot string `json:"blob_kzg_commitments_root"`
|
||||
}
|
||||
|
||||
type SignedExecutionPayloadBid struct {
|
||||
|
||||
@@ -2939,22 +2939,18 @@ func SignedExecutionPayloadBidFromConsensus(b *eth.SignedExecutionPayloadBid) *S
|
||||
}
|
||||
|
||||
func ExecutionPayloadBidFromConsensus(b *eth.ExecutionPayloadBid) *ExecutionPayloadBid {
|
||||
blobKzgCommitments := make([]string, len(b.BlobKzgCommitments))
|
||||
for i := range b.BlobKzgCommitments {
|
||||
blobKzgCommitments[i] = hexutil.Encode(b.BlobKzgCommitments[i])
|
||||
}
|
||||
return &ExecutionPayloadBid{
|
||||
ParentBlockHash: hexutil.Encode(b.ParentBlockHash),
|
||||
ParentBlockRoot: hexutil.Encode(b.ParentBlockRoot),
|
||||
BlockHash: hexutil.Encode(b.BlockHash),
|
||||
PrevRandao: hexutil.Encode(b.PrevRandao),
|
||||
FeeRecipient: hexutil.Encode(b.FeeRecipient),
|
||||
GasLimit: fmt.Sprintf("%d", b.GasLimit),
|
||||
BuilderIndex: fmt.Sprintf("%d", b.BuilderIndex),
|
||||
Slot: fmt.Sprintf("%d", b.Slot),
|
||||
Value: fmt.Sprintf("%d", b.Value),
|
||||
ExecutionPayment: fmt.Sprintf("%d", b.ExecutionPayment),
|
||||
BlobKzgCommitments: blobKzgCommitments,
|
||||
ParentBlockHash: hexutil.Encode(b.ParentBlockHash),
|
||||
ParentBlockRoot: hexutil.Encode(b.ParentBlockRoot),
|
||||
BlockHash: hexutil.Encode(b.BlockHash),
|
||||
PrevRandao: hexutil.Encode(b.PrevRandao),
|
||||
FeeRecipient: hexutil.Encode(b.FeeRecipient),
|
||||
GasLimit: fmt.Sprintf("%d", b.GasLimit),
|
||||
BuilderIndex: fmt.Sprintf("%d", b.BuilderIndex),
|
||||
Slot: fmt.Sprintf("%d", b.Slot),
|
||||
Value: fmt.Sprintf("%d", b.Value),
|
||||
ExecutionPayment: fmt.Sprintf("%d", b.ExecutionPayment),
|
||||
BlobKzgCommitmentsRoot: hexutil.Encode(b.BlobKzgCommitmentsRoot),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3191,30 +3187,22 @@ func (b *ExecutionPayloadBid) ToConsensus() (*eth.ExecutionPayloadBid, error) {
|
||||
if err != nil {
|
||||
return nil, server.NewDecodeError(err, "ExecutionPayment")
|
||||
}
|
||||
err = slice.VerifyMaxLength(b.BlobKzgCommitments, fieldparams.MaxBlobCommitmentsPerBlock)
|
||||
blobKzgCommitmentsRoot, err := bytesutil.DecodeHexWithLength(b.BlobKzgCommitmentsRoot, fieldparams.RootLength)
|
||||
if err != nil {
|
||||
return nil, server.NewDecodeError(err, "BlobKzgCommitments")
|
||||
}
|
||||
blobKzgCommitments := make([][]byte, len(b.BlobKzgCommitments))
|
||||
for i, commitment := range b.BlobKzgCommitments {
|
||||
kzg, err := bytesutil.DecodeHexWithLength(commitment, fieldparams.BLSPubkeyLength)
|
||||
if err != nil {
|
||||
return nil, server.NewDecodeError(err, fmt.Sprintf("BlobKzgCommitments[%d]", i))
|
||||
}
|
||||
blobKzgCommitments[i] = kzg
|
||||
return nil, server.NewDecodeError(err, "BlobKzgCommitmentsRoot")
|
||||
}
|
||||
return ð.ExecutionPayloadBid{
|
||||
ParentBlockHash: parentBlockHash,
|
||||
ParentBlockRoot: parentBlockRoot,
|
||||
BlockHash: blockHash,
|
||||
PrevRandao: prevRandao,
|
||||
FeeRecipient: feeRecipient,
|
||||
GasLimit: gasLimit,
|
||||
BuilderIndex: primitives.BuilderIndex(builderIndex),
|
||||
Slot: primitives.Slot(slot),
|
||||
Value: primitives.Gwei(value),
|
||||
ExecutionPayment: primitives.Gwei(executionPayment),
|
||||
BlobKzgCommitments: blobKzgCommitments,
|
||||
ParentBlockHash: parentBlockHash,
|
||||
ParentBlockRoot: parentBlockRoot,
|
||||
BlockHash: blockHash,
|
||||
PrevRandao: prevRandao,
|
||||
FeeRecipient: feeRecipient,
|
||||
GasLimit: gasLimit,
|
||||
BuilderIndex: primitives.BuilderIndex(builderIndex),
|
||||
Slot: primitives.Slot(slot),
|
||||
Value: primitives.Gwei(value),
|
||||
ExecutionPayment: primitives.Gwei(executionPayment),
|
||||
BlobKzgCommitmentsRoot: blobKzgCommitmentsRoot,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,6 @@ go_library(
|
||||
"receive_blob.go",
|
||||
"receive_block.go",
|
||||
"receive_data_column.go",
|
||||
"receive_execution_payload_envelope.go",
|
||||
"receive_payload_attestation_message.go",
|
||||
"service.go",
|
||||
"setup_forkchoice.go",
|
||||
"tracked_proposer.go",
|
||||
@@ -87,7 +85,6 @@ go_library(
|
||||
"//consensus-types/primitives:go_default_library",
|
||||
"//crypto/bls:go_default_library",
|
||||
"//encoding/bytesutil:go_default_library",
|
||||
"//io/logs:go_default_library",
|
||||
"//math:go_default_library",
|
||||
"//monitoring/tracing:go_default_library",
|
||||
"//monitoring/tracing/trace:go_default_library",
|
||||
|
||||
@@ -101,16 +101,11 @@ func (s *Service) notifyForkchoiceUpdate(ctx context.Context, arg *fcuConfig) (*
|
||||
if len(lastValidHash) == 0 {
|
||||
lastValidHash = defaultLatestValidHash
|
||||
}
|
||||
// this call has guaranteed to have the `headRoot` with its payload in forkchoice.
|
||||
invalidRoots, err := s.cfg.ForkChoiceStore.SetOptimisticToInvalid(ctx, headRoot, headBlk.ParentRoot(), bytesutil.ToBytes32(headPayload.ParentHash()), bytesutil.ToBytes32(lastValidHash))
|
||||
invalidRoots, err := s.cfg.ForkChoiceStore.SetOptimisticToInvalid(ctx, headRoot, headBlk.ParentRoot(), bytesutil.ToBytes32(lastValidHash))
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Could not set head root to invalid")
|
||||
return nil, nil
|
||||
}
|
||||
// TODO: Gloas, we should not include the head root in this call
|
||||
if len(invalidRoots) == 0 || invalidRoots[0] != headRoot {
|
||||
invalidRoots = append([][32]byte{headRoot}, invalidRoots...)
|
||||
}
|
||||
if err := s.removeInvalidBlockAndState(ctx, invalidRoots); err != nil {
|
||||
log.WithError(err).Error("Could not remove invalid block and state")
|
||||
return nil, nil
|
||||
@@ -295,10 +290,10 @@ func (s *Service) notifyNewPayload(ctx context.Context, stVersion int, header in
|
||||
return false, errors.WithMessage(ErrUndefinedExecutionEngineError, err.Error())
|
||||
}
|
||||
|
||||
// pruneInvalidBlock deals with the event that an invalid block was detected by the execution layer
|
||||
func (s *Service) pruneInvalidBlock(ctx context.Context, root, parentRoot, parentHash [32]byte, lvh [32]byte) error {
|
||||
// reportInvalidBlock deals with the event that an invalid block was detected by the execution layer
|
||||
func (s *Service) pruneInvalidBlock(ctx context.Context, root, parentRoot, lvh [32]byte) error {
|
||||
newPayloadInvalidNodeCount.Inc()
|
||||
invalidRoots, err := s.cfg.ForkChoiceStore.SetOptimisticToInvalid(ctx, root, parentRoot, parentHash, lvh)
|
||||
invalidRoots, err := s.cfg.ForkChoiceStore.SetOptimisticToInvalid(ctx, root, parentRoot, lvh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -465,9 +465,9 @@ func Test_NotifyForkchoiceUpdateRecursive_DoublyLinkedTree(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, brd, headRoot)
|
||||
|
||||
// Ensure F and G's full nodes were removed but their empty (consensus) nodes remain, as does E
|
||||
require.Equal(t, true, fcs.HasNode(brf))
|
||||
require.Equal(t, true, fcs.HasNode(brg))
|
||||
// Ensure F and G where removed but their parent E wasn't
|
||||
require.Equal(t, false, fcs.HasNode(brf))
|
||||
require.Equal(t, false, fcs.HasNode(brg))
|
||||
require.Equal(t, true, fcs.HasNode(bre))
|
||||
}
|
||||
|
||||
@@ -703,13 +703,14 @@ func Test_reportInvalidBlock(t *testing.T) {
|
||||
require.NoError(t, fcs.InsertNode(ctx, st, root))
|
||||
|
||||
require.NoError(t, fcs.SetOptimisticToValid(ctx, [32]byte{'A'}))
|
||||
err = service.pruneInvalidBlock(ctx, [32]byte{'D'}, [32]byte{'C'}, [32]byte{'c'}, [32]byte{'a'})
|
||||
err = service.pruneInvalidBlock(ctx, [32]byte{'D'}, [32]byte{'C'}, [32]byte{'a'})
|
||||
require.Equal(t, IsInvalidBlock(err), true)
|
||||
require.Equal(t, InvalidBlockLVH(err), [32]byte{'a'})
|
||||
invalidRoots := InvalidAncestorRoots(err)
|
||||
require.Equal(t, 2, len(invalidRoots))
|
||||
require.Equal(t, 3, len(invalidRoots))
|
||||
require.Equal(t, [32]byte{'D'}, invalidRoots[0])
|
||||
require.Equal(t, [32]byte{'C'}, invalidRoots[1])
|
||||
require.Equal(t, [32]byte{'B'}, invalidRoots[2])
|
||||
}
|
||||
|
||||
func Test_GetPayloadAttribute(t *testing.T) {
|
||||
@@ -784,7 +785,7 @@ func Test_GetPayloadAttributeV2(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_GetPayloadAttributeV3(t *testing.T) {
|
||||
testCases := []struct {
|
||||
var testCases = []struct {
|
||||
name string
|
||||
st bstate.BeaconState
|
||||
}{
|
||||
|
||||
@@ -110,7 +110,7 @@ func VerifyCellKZGProofBatch(commitmentsBytes []Bytes48, cellIndices []uint64, c
|
||||
ckzgCells := make([]ckzg4844.Cell, len(cells))
|
||||
|
||||
for i := range cells {
|
||||
copy(ckzgCells[i][:], cells[i][:])
|
||||
ckzgCells[i] = ckzg4844.Cell(cells[i])
|
||||
}
|
||||
return ckzg4844.VerifyCellKZGProofBatch(commitmentsBytes, cellIndices, ckzgCells, proofsBytes)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
consensus_types "github.com/OffchainLabs/prysm/v7/consensus-types"
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
|
||||
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
|
||||
"github.com/OffchainLabs/prysm/v7/io/logs"
|
||||
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
"github.com/OffchainLabs/prysm/v7/runtime/version"
|
||||
prysmTime "github.com/OffchainLabs/prysm/v7/time"
|
||||
@@ -88,45 +87,36 @@ func logStateTransitionData(b interfaces.ReadOnlyBeaconBlock) error {
|
||||
func logBlockSyncStatus(block interfaces.ReadOnlyBeaconBlock, blockRoot [32]byte, justified, finalized *ethpb.Checkpoint, receivedTime time.Time, genesis time.Time, daWaitedTime time.Duration) error {
|
||||
startTime, err := slots.StartTime(genesis, block.Slot())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get slot start time")
|
||||
return err
|
||||
}
|
||||
parentRoot := block.ParentRoot()
|
||||
blkRoot := fmt.Sprintf("0x%s...", hex.EncodeToString(blockRoot[:])[:8])
|
||||
finalizedRoot := fmt.Sprintf("0x%s...", hex.EncodeToString(finalized.Root)[:8])
|
||||
sinceSlotStartTime := prysmTime.Now().Sub(startTime)
|
||||
|
||||
lessFields := logrus.Fields{
|
||||
"slot": block.Slot(),
|
||||
"block": blkRoot,
|
||||
"finalizedEpoch": finalized.Epoch,
|
||||
"finalizedRoot": finalizedRoot,
|
||||
"epoch": slots.ToEpoch(block.Slot()),
|
||||
"sinceSlotStartTime": sinceSlotStartTime,
|
||||
}
|
||||
moreFields := logrus.Fields{
|
||||
"slot": block.Slot(),
|
||||
"slotInEpoch": block.Slot() % params.BeaconConfig().SlotsPerEpoch,
|
||||
"block": blkRoot,
|
||||
"epoch": slots.ToEpoch(block.Slot()),
|
||||
"justifiedEpoch": justified.Epoch,
|
||||
"justifiedRoot": fmt.Sprintf("0x%s...", hex.EncodeToString(justified.Root)[:8]),
|
||||
"finalizedEpoch": finalized.Epoch,
|
||||
"finalizedRoot": finalizedRoot,
|
||||
"parentRoot": fmt.Sprintf("0x%s...", hex.EncodeToString(parentRoot[:])[:8]),
|
||||
"version": version.String(block.Version()),
|
||||
"sinceSlotStartTime": sinceSlotStartTime,
|
||||
"chainServiceProcessedTime": prysmTime.Now().Sub(receivedTime) - daWaitedTime,
|
||||
"dataAvailabilityWaitedTime": daWaitedTime,
|
||||
}
|
||||
|
||||
level := logs.PackageVerbosity("beacon-chain/blockchain")
|
||||
level := log.Logger.GetLevel()
|
||||
if level >= logrus.DebugLevel {
|
||||
log.WithFields(moreFields).Info("Synced new block")
|
||||
return nil
|
||||
parentRoot := block.ParentRoot()
|
||||
lf := logrus.Fields{
|
||||
"slot": block.Slot(),
|
||||
"slotInEpoch": block.Slot() % params.BeaconConfig().SlotsPerEpoch,
|
||||
"block": fmt.Sprintf("0x%s...", hex.EncodeToString(blockRoot[:])[:8]),
|
||||
"epoch": slots.ToEpoch(block.Slot()),
|
||||
"justifiedEpoch": justified.Epoch,
|
||||
"justifiedRoot": fmt.Sprintf("0x%s...", hex.EncodeToString(justified.Root)[:8]),
|
||||
"finalizedEpoch": finalized.Epoch,
|
||||
"finalizedRoot": fmt.Sprintf("0x%s...", hex.EncodeToString(finalized.Root)[:8]),
|
||||
"parentRoot": fmt.Sprintf("0x%s...", hex.EncodeToString(parentRoot[:])[:8]),
|
||||
"version": version.String(block.Version()),
|
||||
"sinceSlotStartTime": prysmTime.Now().Sub(startTime),
|
||||
"chainServiceProcessedTime": prysmTime.Now().Sub(receivedTime) - daWaitedTime,
|
||||
"dataAvailabilityWaitedTime": daWaitedTime,
|
||||
}
|
||||
log.WithFields(lf).Debug("Synced new block")
|
||||
} else {
|
||||
log.WithFields(logrus.Fields{
|
||||
"slot": block.Slot(),
|
||||
"block": fmt.Sprintf("0x%s...", hex.EncodeToString(blockRoot[:])[:8]),
|
||||
"finalizedEpoch": finalized.Epoch,
|
||||
"finalizedRoot": fmt.Sprintf("0x%s...", hex.EncodeToString(finalized.Root)[:8]),
|
||||
"epoch": slots.ToEpoch(block.Slot()),
|
||||
}).Info("Synced new block")
|
||||
}
|
||||
|
||||
log.WithFields(lessFields).WithField(logs.LogTargetField, logs.LogTargetUser).Info("Synced new block")
|
||||
log.WithFields(moreFields).WithField(logs.LogTargetField, logs.LogTargetEphemeral).Info("Synced new block")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -232,8 +232,7 @@ func (s *Service) onBlockBatch(ctx context.Context, blks []consensusblocks.ROBlo
|
||||
postVersionAndHeaders[i].version,
|
||||
postVersionAndHeaders[i].header, b)
|
||||
if err != nil {
|
||||
// this call does not have the root in forkchoice yet.
|
||||
return s.handleInvalidExecutionError(ctx, err, root, b.Block().ParentRoot(), [32]byte(postVersionAndHeaders[i].header.ParentHash()))
|
||||
return s.handleInvalidExecutionError(ctx, err, root, b.Block().ParentRoot())
|
||||
}
|
||||
if isValidPayload {
|
||||
if err := s.validateMergeTransitionBlock(ctx, preVersionAndHeaders[i].version,
|
||||
@@ -993,9 +992,9 @@ func (s *Service) waitForSync() error {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) handleInvalidExecutionError(ctx context.Context, err error, blockRoot, parentRoot [32]byte, parentHash [32]byte) error {
|
||||
func (s *Service) handleInvalidExecutionError(ctx context.Context, err error, blockRoot, parentRoot [fieldparams.RootLength]byte) error {
|
||||
if IsInvalidBlock(err) && InvalidBlockLVH(err) != [32]byte{} {
|
||||
return s.pruneInvalidBlock(ctx, blockRoot, parentRoot, parentHash, InvalidBlockLVH(err))
|
||||
return s.pruneInvalidBlock(ctx, blockRoot, parentRoot, InvalidBlockLVH(err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2006,7 +2006,6 @@ func TestNoViableHead_Reboot(t *testing.T) {
|
||||
// Check that we have justified the second epoch
|
||||
jc := service.cfg.ForkChoiceStore.JustifiedCheckpoint()
|
||||
require.Equal(t, primitives.Epoch(2), jc.Epoch)
|
||||
time.Sleep(20 * time.Millisecond) // wait for async forkchoice update to be processed
|
||||
|
||||
// import block 19 to find out that the whole chain 13--18 was in fact
|
||||
// invalid
|
||||
|
||||
@@ -633,7 +633,7 @@ func (s *Service) validateExecutionOnBlock(ctx context.Context, ver int, header
|
||||
isValidPayload, err := s.notifyNewPayload(ctx, ver, header, block)
|
||||
if err != nil {
|
||||
s.cfg.ForkChoiceStore.Lock()
|
||||
err = s.handleInvalidExecutionError(ctx, err, block.Root(), block.Block().ParentRoot(), [32]byte(header.BlockHash()))
|
||||
err = s.handleInvalidExecutionError(ctx, err, block.Root(), block.Block().ParentRoot())
|
||||
s.cfg.ForkChoiceStore.Unlock()
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package blockchain
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
|
||||
)
|
||||
|
||||
// ExecutionPayloadEnvelopeReceiver interface defines the methods of chain service for receiving
|
||||
// validated execution payload envelopes.
|
||||
type ExecutionPayloadEnvelopeReceiver interface {
|
||||
ReceiveExecutionPayloadEnvelope(context.Context, interfaces.ROSignedExecutionPayloadEnvelope) error
|
||||
}
|
||||
|
||||
// ReceiveExecutionPayloadEnvelope accepts a signed execution payload envelope.
|
||||
func (s *Service) ReceiveExecutionPayloadEnvelope(_ context.Context, _ interfaces.ROSignedExecutionPayloadEnvelope) error {
|
||||
// TODO: wire into execution payload envelope processing pipeline.
|
||||
return nil
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package blockchain
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
)
|
||||
|
||||
// PayloadAttestationReceiver interface defines the methods of chain service for receiving
|
||||
// validated payload attestation messages.
|
||||
type PayloadAttestationReceiver interface {
|
||||
ReceivePayloadAttestationMessage(context.Context, *ethpb.PayloadAttestationMessage) error
|
||||
}
|
||||
|
||||
// ReceivePayloadAttestationMessage accepts a payload attestation message.
|
||||
func (s *Service) ReceivePayloadAttestationMessage(ctx context.Context, a *ethpb.PayloadAttestationMessage) error {
|
||||
// TODO: Handle payload attestation message processing once Gloas is fully wired.
|
||||
return nil
|
||||
}
|
||||
@@ -89,7 +89,7 @@ func (mb *mockBroadcaster) BroadcastLightClientFinalityUpdate(_ context.Context,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mb *mockBroadcaster) BroadcastDataColumnSidecars(_ context.Context, _ []blocks.VerifiedRODataColumn) error {
|
||||
func (mb *mockBroadcaster) BroadcastDataColumnSidecars(_ context.Context, _ []blocks.VerifiedRODataColumn, _ []blocks.PartialDataColumn) error {
|
||||
mb.broadcastCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -757,16 +757,6 @@ func (c *ChainService) ReceiveDataColumns(dcs []blocks.VerifiedRODataColumn) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReceivePayloadAttestationMessage implements the same method in the chain service.
|
||||
func (c *ChainService) ReceivePayloadAttestationMessage(_ context.Context, _ *ethpb.PayloadAttestationMessage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReceiveExecutionPayloadEnvelope implements the same method in the chain service.
|
||||
func (c *ChainService) ReceiveExecutionPayloadEnvelope(_ context.Context, _ interfaces.ROSignedExecutionPayloadEnvelope) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DependentRootForEpoch mocks the same method in the chain service
|
||||
func (c *ChainService) DependentRootForEpoch(_ [32]byte, _ primitives.Epoch) ([32]byte, error) {
|
||||
return c.TargetRoot, nil
|
||||
|
||||
2
beacon-chain/cache/BUILD.bazel
vendored
2
beacon-chain/cache/BUILD.bazel
vendored
@@ -17,7 +17,6 @@ go_library(
|
||||
"error.go",
|
||||
"interfaces.go",
|
||||
"log.go",
|
||||
"payload_attestation.go",
|
||||
"payload_id.go",
|
||||
"proposer_indices.go",
|
||||
"proposer_indices_disabled.go", # keep
|
||||
@@ -77,7 +76,6 @@ go_test(
|
||||
"checkpoint_state_test.go",
|
||||
"committee_fuzz_test.go",
|
||||
"committee_test.go",
|
||||
"payload_attestation_test.go",
|
||||
"payload_id_test.go",
|
||||
"private_access_test.go",
|
||||
"proposer_indices_test.go",
|
||||
|
||||
53
beacon-chain/cache/payload_attestation.go
vendored
53
beacon-chain/cache/payload_attestation.go
vendored
@@ -1,53 +0,0 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
||||
)
|
||||
|
||||
// PayloadAttestationCache tracks seen payload attestation messages for a single slot.
|
||||
type PayloadAttestationCache struct {
|
||||
slot primitives.Slot
|
||||
seen map[primitives.ValidatorIndex]struct{}
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Seen returns true if a vote for the given slot has already been
|
||||
// processed for this validator index.
|
||||
func (p *PayloadAttestationCache) Seen(slot primitives.Slot, idx primitives.ValidatorIndex) bool {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
if p.slot != slot {
|
||||
return false
|
||||
}
|
||||
if p.seen == nil {
|
||||
return false
|
||||
}
|
||||
_, ok := p.seen[idx]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Add marks the given slot and validator index as seen.
|
||||
// This function assumes that the message has already been validated.
|
||||
func (p *PayloadAttestationCache) Add(slot primitives.Slot, idx primitives.ValidatorIndex) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.slot != slot {
|
||||
p.slot = slot
|
||||
p.seen = make(map[primitives.ValidatorIndex]struct{})
|
||||
}
|
||||
if p.seen == nil {
|
||||
p.seen = make(map[primitives.ValidatorIndex]struct{})
|
||||
}
|
||||
p.seen[idx] = struct{}{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clear clears the internal cache.
|
||||
func (p *PayloadAttestationCache) Clear() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.slot = 0
|
||||
p.seen = nil
|
||||
}
|
||||
48
beacon-chain/cache/payload_attestation_test.go
vendored
48
beacon-chain/cache/payload_attestation_test.go
vendored
@@ -1,48 +0,0 @@
|
||||
package cache_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/cache"
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPayloadAttestationCache_SeenAndAdd(t *testing.T) {
|
||||
var c cache.PayloadAttestationCache
|
||||
slot1 := primitives.Slot(1)
|
||||
slot2 := primitives.Slot(2)
|
||||
idx1 := primitives.ValidatorIndex(3)
|
||||
idx2 := primitives.ValidatorIndex(4)
|
||||
|
||||
require.False(t, c.Seen(slot1, idx1))
|
||||
|
||||
require.NoError(t, c.Add(slot1, idx1))
|
||||
require.True(t, c.Seen(slot1, idx1))
|
||||
require.False(t, c.Seen(slot1, idx2))
|
||||
require.False(t, c.Seen(slot2, idx1))
|
||||
|
||||
require.NoError(t, c.Add(slot1, idx2))
|
||||
require.True(t, c.Seen(slot1, idx1))
|
||||
require.True(t, c.Seen(slot1, idx2))
|
||||
|
||||
require.NoError(t, c.Add(slot2, idx1))
|
||||
require.True(t, c.Seen(slot2, idx1))
|
||||
require.False(t, c.Seen(slot1, idx1))
|
||||
require.False(t, c.Seen(slot1, idx2))
|
||||
}
|
||||
|
||||
func TestPayloadAttestationCache_Clear(t *testing.T) {
|
||||
var c cache.PayloadAttestationCache
|
||||
slot := primitives.Slot(10)
|
||||
idx := primitives.ValidatorIndex(42)
|
||||
|
||||
require.NoError(t, c.Add(slot, idx))
|
||||
require.True(t, c.Seen(slot, idx))
|
||||
|
||||
c.Clear()
|
||||
require.False(t, c.Seen(slot, idx))
|
||||
|
||||
require.NoError(t, c.Add(slot, idx))
|
||||
require.True(t, c.Seen(slot, idx))
|
||||
}
|
||||
@@ -20,7 +20,6 @@ go_library(
|
||||
"//beacon-chain/core/blocks:go_default_library",
|
||||
"//beacon-chain/core/epoch:go_default_library",
|
||||
"//beacon-chain/core/epoch/precompute:go_default_library",
|
||||
"//beacon-chain/core/gloas:go_default_library",
|
||||
"//beacon-chain/core/helpers:go_default_library",
|
||||
"//beacon-chain/core/signing:go_default_library",
|
||||
"//beacon-chain/core/time:go_default_library",
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/blocks"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/time"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
|
||||
@@ -76,11 +75,7 @@ func ProcessAttestationNoVerifySignature(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := beaconState.UpdatePendingPaymentWeight(att, indices, participatedFlags); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to update pending payment weight")
|
||||
}
|
||||
|
||||
return SetParticipationAndRewardProposer(ctx, beaconState, att.GetData().Target.Epoch, indices, participatedFlags, totalBalance, att)
|
||||
return SetParticipationAndRewardProposer(ctx, beaconState, att.GetData().Target.Epoch, indices, participatedFlags, totalBalance)
|
||||
}
|
||||
|
||||
// SetParticipationAndRewardProposer retrieves and sets the epoch participation bits in state. Based on the epoch participation, it rewards
|
||||
@@ -110,9 +105,7 @@ func SetParticipationAndRewardProposer(
|
||||
beaconState state.BeaconState,
|
||||
targetEpoch primitives.Epoch,
|
||||
indices []uint64,
|
||||
participatedFlags map[uint8]bool,
|
||||
totalBalance uint64,
|
||||
att ethpb.Att) (state.BeaconState, error) {
|
||||
participatedFlags map[uint8]bool, totalBalance uint64) (state.BeaconState, error) {
|
||||
var proposerRewardNumerator uint64
|
||||
currentEpoch := time.CurrentEpoch(beaconState)
|
||||
var stateErr error
|
||||
@@ -306,19 +299,6 @@ func AttestationParticipationFlagIndices(beaconState state.ReadOnlyBeaconState,
|
||||
participatedFlags[targetFlagIndex] = true
|
||||
}
|
||||
matchedSrcTgtHead := matchedHead && matchedSrcTgt
|
||||
|
||||
var beaconBlockRoot [32]byte
|
||||
copy(beaconBlockRoot[:], data.BeaconBlockRoot)
|
||||
matchingPayload, err := gloas.MatchingPayload(
|
||||
beaconState,
|
||||
beaconBlockRoot,
|
||||
data.Slot,
|
||||
uint64(data.CommitteeIndex),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matchedSrcTgtHead = matchedSrcTgtHead && matchingPayload
|
||||
if matchedSrcTgtHead && delay == cfg.MinAttestationInclusionDelay {
|
||||
participatedFlags[headFlagIndex] = true
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package altair_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/OffchainLabs/go-bitfield"
|
||||
@@ -558,7 +556,7 @@ func TestSetParticipationAndRewardProposer(t *testing.T) {
|
||||
|
||||
b, err := helpers.TotalActiveBalance(beaconState)
|
||||
require.NoError(t, err)
|
||||
st, err := altair.SetParticipationAndRewardProposer(t.Context(), beaconState, test.epoch, test.indices, test.participatedFlags, b, ðpb.Attestation{})
|
||||
st, err := altair.SetParticipationAndRewardProposer(t.Context(), beaconState, test.epoch, test.indices, test.participatedFlags, b)
|
||||
require.NoError(t, err)
|
||||
|
||||
i, err := helpers.BeaconProposerIndex(t.Context(), st)
|
||||
@@ -777,67 +775,11 @@ func TestAttestationParticipationFlagIndices(t *testing.T) {
|
||||
headFlagIndex: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gloas same-slot committee index non-zero errors",
|
||||
inputState: func() state.BeaconState {
|
||||
stateSlot := primitives.Slot(5)
|
||||
slot := primitives.Slot(3)
|
||||
targetRoot := bytes.Repeat([]byte{0xAA}, 32)
|
||||
headRoot := bytes.Repeat([]byte{0xBB}, 32)
|
||||
prevRoot := bytes.Repeat([]byte{0xCC}, 32)
|
||||
return buildGloasStateForFlags(t, stateSlot, slot, targetRoot, headRoot, prevRoot, 0, 0)
|
||||
}(),
|
||||
inputData: ðpb.AttestationData{
|
||||
Slot: 3,
|
||||
CommitteeIndex: 1, // invalid for same-slot
|
||||
BeaconBlockRoot: bytes.Repeat([]byte{0xBB}, 32),
|
||||
Source: ðpb.Checkpoint{Root: bytes.Repeat([]byte{0xDD}, 32)},
|
||||
Target: ðpb.Checkpoint{
|
||||
Epoch: 0,
|
||||
Root: bytes.Repeat([]byte{0xAA}, 32),
|
||||
},
|
||||
},
|
||||
inputDelay: 1,
|
||||
participationIndices: nil,
|
||||
},
|
||||
{
|
||||
name: "gloas payload availability matches committee index",
|
||||
inputState: func() state.BeaconState {
|
||||
stateSlot := primitives.Slot(5)
|
||||
slot := primitives.Slot(3)
|
||||
targetRoot := bytes.Repeat([]byte{0xAA}, 32)
|
||||
headRoot := bytes.Repeat([]byte{0xBB}, 32)
|
||||
// Same prev root to make SameSlotAttestation false and use payload availability.
|
||||
return buildGloasStateForFlags(t, stateSlot, slot, targetRoot, headRoot, headRoot, 1, slot)
|
||||
}(),
|
||||
inputData: ðpb.AttestationData{
|
||||
Slot: 3,
|
||||
CommitteeIndex: 1,
|
||||
BeaconBlockRoot: bytes.Repeat([]byte{0xBB}, 32),
|
||||
Source: ðpb.Checkpoint{Root: bytes.Repeat([]byte{0xDD}, 32)},
|
||||
Target: ðpb.Checkpoint{
|
||||
Epoch: 0,
|
||||
Root: bytes.Repeat([]byte{0xAA}, 32),
|
||||
},
|
||||
},
|
||||
inputDelay: 1,
|
||||
participationIndices: map[uint8]bool{
|
||||
sourceFlagIndex: true,
|
||||
targetFlagIndex: true,
|
||||
headFlagIndex: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
flagIndices, err := altair.AttestationParticipationFlagIndices(test.inputState, test.inputData, test.inputDelay)
|
||||
if test.participationIndices == nil {
|
||||
require.ErrorContains(t, "committee index", err)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err)
|
||||
if !reflect.DeepEqual(test.participationIndices, flagIndices) {
|
||||
t.Fatalf("unexpected participation indices: got %v want %v", flagIndices, test.participationIndices)
|
||||
}
|
||||
require.DeepEqual(t, test.participationIndices, flagIndices)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -916,61 +858,3 @@ func TestMatchingStatus(t *testing.T) {
|
||||
require.Equal(t, test.matchedHead, head)
|
||||
}
|
||||
}
|
||||
|
||||
func buildGloasStateForFlags(t *testing.T, stateSlot, slot primitives.Slot, targetRoot, headRoot, prevRoot []byte, availabilityBit uint8, availabilitySlot primitives.Slot) state.BeaconState {
|
||||
t.Helper()
|
||||
|
||||
cfg := params.BeaconConfig()
|
||||
blockRoots := make([][]byte, cfg.SlotsPerHistoricalRoot)
|
||||
blockRoots[0] = targetRoot
|
||||
blockRoots[slot%cfg.SlotsPerHistoricalRoot] = headRoot
|
||||
blockRoots[(slot-1)%cfg.SlotsPerHistoricalRoot] = prevRoot
|
||||
|
||||
stateRoots := make([][]byte, cfg.SlotsPerHistoricalRoot)
|
||||
for i := range stateRoots {
|
||||
stateRoots[i] = make([]byte, fieldparams.RootLength)
|
||||
}
|
||||
randaoMixes := make([][]byte, cfg.EpochsPerHistoricalVector)
|
||||
for i := range randaoMixes {
|
||||
randaoMixes[i] = make([]byte, fieldparams.RootLength)
|
||||
}
|
||||
|
||||
execPayloadAvailability := make([]byte, cfg.SlotsPerHistoricalRoot/8)
|
||||
idx := availabilitySlot % cfg.SlotsPerHistoricalRoot
|
||||
byteIndex := idx / 8
|
||||
bitIndex := idx % 8
|
||||
if availabilityBit == 1 {
|
||||
execPayloadAvailability[byteIndex] |= 1 << bitIndex
|
||||
}
|
||||
|
||||
checkpointRoot := bytes.Repeat([]byte{0xDD}, fieldparams.RootLength)
|
||||
justified := ðpb.Checkpoint{Root: checkpointRoot}
|
||||
|
||||
stProto := ðpb.BeaconStateGloas{
|
||||
Slot: stateSlot,
|
||||
GenesisValidatorsRoot: bytes.Repeat([]byte{0x11}, fieldparams.RootLength),
|
||||
BlockRoots: blockRoots,
|
||||
StateRoots: stateRoots,
|
||||
RandaoMixes: randaoMixes,
|
||||
ExecutionPayloadAvailability: execPayloadAvailability,
|
||||
CurrentJustifiedCheckpoint: justified,
|
||||
PreviousJustifiedCheckpoint: justified,
|
||||
Validators: []*ethpb.Validator{
|
||||
{
|
||||
EffectiveBalance: cfg.MinActivationBalance,
|
||||
WithdrawalCredentials: append([]byte{cfg.ETH1AddressWithdrawalPrefixByte}, bytes.Repeat([]byte{0x01}, 31)...),
|
||||
},
|
||||
},
|
||||
Balances: []uint64{cfg.MinActivationBalance},
|
||||
BuilderPendingPayments: make([]*ethpb.BuilderPendingPayment, cfg.SlotsPerEpoch*2),
|
||||
Fork: ðpb.Fork{
|
||||
CurrentVersion: bytes.Repeat([]byte{0x01}, 4),
|
||||
PreviousVersion: bytes.Repeat([]byte{0x01}, 4),
|
||||
Epoch: 0,
|
||||
},
|
||||
}
|
||||
|
||||
beaconState, err := state_native.InitializeFromProtoGloas(stProto)
|
||||
require.NoError(t, err)
|
||||
return beaconState
|
||||
}
|
||||
|
||||
@@ -111,21 +111,10 @@ func VerifyAttestationNoVerifySignature(
|
||||
var indexedAtt ethpb.IndexedAtt
|
||||
|
||||
if att.Version() >= version.Electra {
|
||||
ci := att.GetData().CommitteeIndex
|
||||
// Spec v1.7.0-alpha pseudocode:
|
||||
//
|
||||
// # [Modified in Gloas:EIP7732]
|
||||
// assert data.index < 2
|
||||
//
|
||||
if beaconState.Version() >= version.Gloas {
|
||||
if ci >= 2 {
|
||||
return fmt.Errorf("incorrect committee index %d", ci)
|
||||
}
|
||||
} else {
|
||||
if ci != 0 {
|
||||
return errors.New("committee index must be 0 between Electra and Gloas forks")
|
||||
}
|
||||
if att.GetData().CommitteeIndex != 0 {
|
||||
return errors.New("committee index must be 0 post-Electra")
|
||||
}
|
||||
|
||||
aggBits := att.GetAggregationBits()
|
||||
committeeIndices := att.CommitteeBitsVal().BitIndices()
|
||||
committees := make([][]primitives.ValidatorIndex, len(committeeIndices))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package blocks_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
@@ -263,7 +262,7 @@ func TestVerifyAttestationNoVerifySignature_Electra(t *testing.T) {
|
||||
CommitteeBits: bitfield.NewBitvector64(),
|
||||
}
|
||||
err = blocks.VerifyAttestationNoVerifySignature(context.TODO(), beaconState, att)
|
||||
assert.ErrorContains(t, "committee index must be 0", err)
|
||||
assert.ErrorContains(t, "committee index must be 0 post-Electra", err)
|
||||
})
|
||||
t.Run("index of committee too big", func(t *testing.T) {
|
||||
aggBits := bitfield.NewBitlist(3)
|
||||
@@ -315,75 +314,6 @@ func TestVerifyAttestationNoVerifySignature_Electra(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestVerifyAttestationNoVerifySignature_GloasCommitteeIndexLimit(t *testing.T) {
|
||||
cfg := params.BeaconConfig()
|
||||
stateSlot := cfg.MinAttestationInclusionDelay + 1
|
||||
|
||||
blockRoots := make([][]byte, cfg.SlotsPerHistoricalRoot)
|
||||
for i := range blockRoots {
|
||||
blockRoots[i] = make([]byte, fieldparams.RootLength)
|
||||
}
|
||||
stateRoots := make([][]byte, cfg.SlotsPerHistoricalRoot)
|
||||
for i := range stateRoots {
|
||||
stateRoots[i] = make([]byte, fieldparams.RootLength)
|
||||
}
|
||||
randaoMixes := make([][]byte, cfg.EpochsPerHistoricalVector)
|
||||
for i := range randaoMixes {
|
||||
randaoMixes[i] = make([]byte, fieldparams.RootLength)
|
||||
}
|
||||
|
||||
checkpointRoot := bytes.Repeat([]byte{0xAA}, fieldparams.RootLength)
|
||||
justified := ðpb.Checkpoint{Epoch: 0, Root: checkpointRoot}
|
||||
|
||||
gloasStateProto := ðpb.BeaconStateGloas{
|
||||
Slot: stateSlot,
|
||||
GenesisValidatorsRoot: bytes.Repeat([]byte{0x11}, fieldparams.RootLength),
|
||||
BlockRoots: blockRoots,
|
||||
StateRoots: stateRoots,
|
||||
RandaoMixes: randaoMixes,
|
||||
ExecutionPayloadAvailability: make([]byte, cfg.SlotsPerHistoricalRoot/8),
|
||||
CurrentJustifiedCheckpoint: justified,
|
||||
PreviousJustifiedCheckpoint: justified,
|
||||
Validators: []*ethpb.Validator{
|
||||
{
|
||||
EffectiveBalance: cfg.MinActivationBalance,
|
||||
WithdrawalCredentials: append([]byte{cfg.ETH1AddressWithdrawalPrefixByte}, bytes.Repeat([]byte{0x01}, 31)...),
|
||||
},
|
||||
},
|
||||
Balances: []uint64{cfg.MinActivationBalance},
|
||||
BuilderPendingPayments: make([]*ethpb.BuilderPendingPayment, cfg.SlotsPerEpoch*2),
|
||||
Fork: ðpb.Fork{
|
||||
CurrentVersion: bytes.Repeat([]byte{0x01}, 4),
|
||||
PreviousVersion: bytes.Repeat([]byte{0x01}, 4),
|
||||
Epoch: 0,
|
||||
},
|
||||
}
|
||||
|
||||
beaconState, err := state_native.InitializeFromProtoGloas(gloasStateProto)
|
||||
require.NoError(t, err)
|
||||
|
||||
committeeBits := bitfield.NewBitvector64()
|
||||
committeeBits.SetBitAt(0, true)
|
||||
aggBits := bitfield.NewBitlist(1)
|
||||
aggBits.SetBitAt(0, true)
|
||||
|
||||
att := ðpb.AttestationElectra{
|
||||
Data: ðpb.AttestationData{
|
||||
Slot: 0,
|
||||
CommitteeIndex: 2, // invalid for Gloas (must be <2)
|
||||
BeaconBlockRoot: blockRoots[0],
|
||||
Source: justified,
|
||||
Target: justified,
|
||||
},
|
||||
AggregationBits: aggBits,
|
||||
CommitteeBits: committeeBits,
|
||||
Signature: bytes.Repeat([]byte{0x00}, fieldparams.BLSSignatureLength),
|
||||
}
|
||||
|
||||
err = blocks.VerifyAttestationNoVerifySignature(context.TODO(), beaconState, att)
|
||||
assert.ErrorContains(t, "incorrect committee index 2", err)
|
||||
}
|
||||
|
||||
func TestConvertToIndexed_OK(t *testing.T) {
|
||||
helpers.ClearCache()
|
||||
validators := make([]*ethpb.Validator, 2*params.BeaconConfig().SlotsPerEpoch)
|
||||
@@ -653,7 +583,6 @@ func TestVerifyAttestations_HandlesPlannedFork(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRetrieveAttestationSignatureSet_VerifiesMultipleAttestations(t *testing.T) {
|
||||
helpers.ClearCache()
|
||||
ctx := t.Context()
|
||||
numOfValidators := uint64(params.BeaconConfig().SlotsPerEpoch.Mul(4))
|
||||
validators := make([]*ethpb.Validator, numOfValidators)
|
||||
|
||||
@@ -3,11 +3,7 @@ load("@prysm//tools/go:def.bzl", "go_library", "go_test")
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"attestation.go",
|
||||
"bid.go",
|
||||
"deposit_request.go",
|
||||
"log.go",
|
||||
"payload.go",
|
||||
"payload_attestation.go",
|
||||
"pending_payment.go",
|
||||
"proposer_slashing.go",
|
||||
@@ -16,7 +12,6 @@ go_library(
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//beacon-chain/core/helpers:go_default_library",
|
||||
"//beacon-chain/core/requests:go_default_library",
|
||||
"//beacon-chain/core/signing:go_default_library",
|
||||
"//beacon-chain/core/time:go_default_library",
|
||||
"//beacon-chain/state:go_default_library",
|
||||
@@ -30,23 +25,17 @@ go_library(
|
||||
"//crypto/bls/common:go_default_library",
|
||||
"//crypto/hash:go_default_library",
|
||||
"//encoding/bytesutil:go_default_library",
|
||||
"//proto/engine/v1:go_default_library",
|
||||
"//proto/prysm/v1alpha1:go_default_library",
|
||||
"//runtime/version:go_default_library",
|
||||
"//time/slots:go_default_library",
|
||||
"@com_github_pkg_errors//:go_default_library",
|
||||
"@com_github_sirupsen_logrus//:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"attestation_test.go",
|
||||
"bid_test.go",
|
||||
"deposit_request_test.go",
|
||||
"payload_attestation_test.go",
|
||||
"payload_test.go",
|
||||
"pending_payment_test.go",
|
||||
"proposer_slashing_test.go",
|
||||
],
|
||||
@@ -56,7 +45,6 @@ go_test(
|
||||
"//beacon-chain/core/signing:go_default_library",
|
||||
"//beacon-chain/state:go_default_library",
|
||||
"//beacon-chain/state/state-native:go_default_library",
|
||||
"//beacon-chain/state/testing:go_default_library",
|
||||
"//config/params:go_default_library",
|
||||
"//consensus-types/blocks:go_default_library",
|
||||
"//consensus-types/interfaces:go_default_library",
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package gloas
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
||||
"github.com/OffchainLabs/prysm/v7/runtime/version"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// MatchingPayload returns true if the attestation's committee index matches the expected payload index.
|
||||
//
|
||||
// For pre-Gloas forks, this always returns true.
|
||||
//
|
||||
// Spec v1.7.0-alpha (pseudocode):
|
||||
//
|
||||
// # [New in Gloas:EIP7732]
|
||||
// if is_attestation_same_slot(state, data):
|
||||
// assert data.index == 0
|
||||
// payload_matches = True
|
||||
// else:
|
||||
// slot_index = data.slot % SLOTS_PER_HISTORICAL_ROOT
|
||||
// payload_index = state.execution_payload_availability[slot_index]
|
||||
// payload_matches = data.index == payload_index
|
||||
func MatchingPayload(
|
||||
beaconState state.ReadOnlyBeaconState,
|
||||
beaconBlockRoot [32]byte,
|
||||
slot primitives.Slot,
|
||||
committeeIndex uint64,
|
||||
) (bool, error) {
|
||||
if beaconState.Version() < version.Gloas {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
sameSlot, err := beaconState.IsAttestationSameSlot(beaconBlockRoot, slot)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to get same slot attestation status")
|
||||
}
|
||||
if sameSlot {
|
||||
if committeeIndex != 0 {
|
||||
return false, fmt.Errorf("committee index %d for same slot attestation must be 0", committeeIndex)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
executionPayloadAvail, err := beaconState.ExecutionPayloadAvailability(slot)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to get execution payload availability status")
|
||||
}
|
||||
return executionPayloadAvail == committeeIndex, nil
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package gloas
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native"
|
||||
"github.com/OffchainLabs/prysm/v7/config/params"
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
||||
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
"github.com/OffchainLabs/prysm/v7/runtime/version"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/require"
|
||||
)
|
||||
|
||||
func buildStateWithBlockRoots(t *testing.T, stateSlot primitives.Slot, roots map[primitives.Slot][]byte) *state_native.BeaconState {
|
||||
t.Helper()
|
||||
|
||||
cfg := params.BeaconConfig()
|
||||
blockRoots := make([][]byte, cfg.SlotsPerHistoricalRoot)
|
||||
for slot, root := range roots {
|
||||
blockRoots[slot%cfg.SlotsPerHistoricalRoot] = root
|
||||
}
|
||||
|
||||
stProto := ðpb.BeaconStateGloas{
|
||||
Slot: stateSlot,
|
||||
BlockRoots: blockRoots,
|
||||
}
|
||||
|
||||
state, err := state_native.InitializeFromProtoGloas(stProto)
|
||||
require.NoError(t, err)
|
||||
return state.(*state_native.BeaconState)
|
||||
}
|
||||
|
||||
func TestMatchingPayload(t *testing.T) {
|
||||
t.Run("pre-gloas always true", func(t *testing.T) {
|
||||
stIface, err := state_native.InitializeFromProtoElectra(ðpb.BeaconStateElectra{})
|
||||
require.NoError(t, err)
|
||||
|
||||
ok, err := MatchingPayload(stIface, [32]byte{}, 0, 123)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, true, ok)
|
||||
})
|
||||
|
||||
t.Run("same slot requires committee index 0", func(t *testing.T) {
|
||||
root := bytes.Repeat([]byte{0xAA}, 32)
|
||||
state := buildStateWithBlockRoots(t, 6, map[primitives.Slot][]byte{
|
||||
4: root,
|
||||
3: bytes.Repeat([]byte{0xBB}, 32),
|
||||
})
|
||||
|
||||
var rootArr [32]byte
|
||||
copy(rootArr[:], root)
|
||||
|
||||
ok, err := MatchingPayload(state, rootArr, 4, 1)
|
||||
require.ErrorContains(t, "committee index", err)
|
||||
require.Equal(t, false, ok)
|
||||
})
|
||||
|
||||
t.Run("same slot matches when committee index is 0", func(t *testing.T) {
|
||||
root := bytes.Repeat([]byte{0xAA}, 32)
|
||||
state := buildStateWithBlockRoots(t, 6, map[primitives.Slot][]byte{
|
||||
4: root,
|
||||
3: bytes.Repeat([]byte{0xBB}, 32),
|
||||
})
|
||||
|
||||
var rootArr [32]byte
|
||||
copy(rootArr[:], root)
|
||||
|
||||
ok, err := MatchingPayload(state, rootArr, 4, 0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, true, ok)
|
||||
})
|
||||
|
||||
t.Run("non same slot checks payload availability", func(t *testing.T) {
|
||||
cfg := params.BeaconConfig()
|
||||
root := bytes.Repeat([]byte{0xAA}, 32)
|
||||
blockRoots := make([][]byte, cfg.SlotsPerHistoricalRoot)
|
||||
blockRoots[4%cfg.SlotsPerHistoricalRoot] = bytes.Repeat([]byte{0xCC}, 32)
|
||||
blockRoots[3%cfg.SlotsPerHistoricalRoot] = bytes.Repeat([]byte{0xBB}, 32)
|
||||
|
||||
availability := make([]byte, cfg.SlotsPerHistoricalRoot/8)
|
||||
slotIndex := uint64(4)
|
||||
availability[slotIndex/8] = byte(1 << (slotIndex % 8))
|
||||
|
||||
stIface, err := state_native.InitializeFromProtoGloas(ðpb.BeaconStateGloas{
|
||||
Slot: 6,
|
||||
BlockRoots: blockRoots,
|
||||
ExecutionPayloadAvailability: availability,
|
||||
Fork: ðpb.Fork{
|
||||
CurrentVersion: bytes.Repeat([]byte{0x66}, 4),
|
||||
PreviousVersion: bytes.Repeat([]byte{0x66}, 4),
|
||||
Epoch: 0,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
state := stIface.(*state_native.BeaconState)
|
||||
require.Equal(t, version.Gloas, state.Version())
|
||||
|
||||
var rootArr [32]byte
|
||||
copy(rootArr[:], root)
|
||||
|
||||
ok, err := MatchingPayload(state, rootArr, 4, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, true, ok)
|
||||
|
||||
ok, err = MatchingPayload(state, rootArr, 4, 0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, false, ok)
|
||||
})
|
||||
}
|
||||
@@ -17,56 +17,27 @@ import (
|
||||
)
|
||||
|
||||
// ProcessExecutionPayloadBid processes a signed execution payload bid in the Gloas fork.
|
||||
// Spec v1.7.0-alpha.0 (pseudocode):
|
||||
// process_execution_payload_bid(state: BeaconState, block: BeaconBlock):
|
||||
//
|
||||
// <spec fn="process_execution_payload_bid" fork="gloas" hash="823c9f3a">
|
||||
// def process_execution_payload_bid(state: BeaconState, block: BeaconBlock) -> None:
|
||||
// signed_bid = block.body.signed_execution_payload_bid
|
||||
// bid = signed_bid.message
|
||||
// builder_index = bid.builder_index
|
||||
// amount = bid.value
|
||||
//
|
||||
// # For self-builds, amount must be zero regardless of withdrawal credential prefix
|
||||
// if builder_index == BUILDER_INDEX_SELF_BUILD:
|
||||
// assert amount == 0
|
||||
// assert signed_bid.signature == bls.G2_POINT_AT_INFINITY
|
||||
// else:
|
||||
// # Verify that the builder is active
|
||||
// assert is_active_builder(state, builder_index)
|
||||
// # Verify that the builder has funds to cover the bid
|
||||
// assert can_builder_cover_bid(state, builder_index, amount)
|
||||
// # Verify that the bid signature is valid
|
||||
// assert verify_execution_payload_bid_signature(state, signed_bid)
|
||||
//
|
||||
// # Verify commitments are under limit
|
||||
// assert (
|
||||
// len(bid.blob_kzg_commitments)
|
||||
// <= get_blob_parameters(get_current_epoch(state)).max_blobs_per_block
|
||||
// )
|
||||
//
|
||||
// # Verify that the bid is for the current slot
|
||||
// assert bid.slot == block.slot
|
||||
// # Verify that the bid is for the right parent block
|
||||
// assert bid.parent_block_hash == state.latest_block_hash
|
||||
// assert bid.parent_block_root == block.parent_root
|
||||
// assert bid.prev_randao == get_randao_mix(state, get_current_epoch(state))
|
||||
//
|
||||
// # Record the pending payment if there is some payment
|
||||
// if amount > 0:
|
||||
// pending_payment = BuilderPendingPayment(
|
||||
// weight=0,
|
||||
// withdrawal=BuilderPendingWithdrawal(
|
||||
// fee_recipient=bid.fee_recipient,
|
||||
// amount=amount,
|
||||
// builder_index=builder_index,
|
||||
// ),
|
||||
// )
|
||||
// state.builder_pending_payments[SLOTS_PER_EPOCH + bid.slot % SLOTS_PER_EPOCH] = (
|
||||
// pending_payment
|
||||
// )
|
||||
//
|
||||
// # Cache the signed execution payload bid
|
||||
// state.latest_execution_payload_bid = bid
|
||||
// </spec>
|
||||
// signed_bid = block.body.signed_execution_payload_bid
|
||||
// bid = signed_bid.message
|
||||
// builder_index = bid.builder_index
|
||||
// amount = bid.value
|
||||
// if builder_index == BUILDER_INDEX_SELF_BUILD:
|
||||
// assert amount == 0
|
||||
// assert signed_bid.signature == G2_POINT_AT_INFINITY
|
||||
// else:
|
||||
// assert is_active_builder(state, builder_index)
|
||||
// assert can_builder_cover_bid(state, builder_index, amount)
|
||||
// assert verify_execution_payload_bid_signature(state, signed_bid)
|
||||
// assert bid.slot == block.slot
|
||||
// assert bid.parent_block_hash == state.latest_block_hash
|
||||
// assert bid.parent_block_root == block.parent_root
|
||||
// assert bid.prev_randao == get_randao_mix(state, get_current_epoch(state))
|
||||
// if amount > 0:
|
||||
// state.builder_pending_payments[...] = BuilderPendingPayment(weight=0, withdrawal=BuilderPendingWithdrawal(fee_recipient=bid.fee_recipient, amount=amount, builder_index=builder_index))
|
||||
// state.latest_execution_payload_bid = bid
|
||||
func ProcessExecutionPayloadBid(st state.BeaconState, block interfaces.ReadOnlyBeaconBlock) error {
|
||||
signedBid, err := block.Body().SignedExecutionPayloadBid()
|
||||
if err != nil {
|
||||
@@ -115,12 +86,6 @@ func ProcessExecutionPayloadBid(st state.BeaconState, block interfaces.ReadOnlyB
|
||||
}
|
||||
}
|
||||
|
||||
maxBlobsPerBlock := params.BeaconConfig().MaxBlobsPerBlockAtEpoch(slots.ToEpoch(block.Slot()))
|
||||
commitmentCount := bid.BlobKzgCommitmentCount()
|
||||
if commitmentCount > uint64(maxBlobsPerBlock) {
|
||||
return fmt.Errorf("bid has %d blob KZG commitments over max %d", commitmentCount, maxBlobsPerBlock)
|
||||
}
|
||||
|
||||
if err := validateBidConsistency(st, bid, block); err != nil {
|
||||
return errors.Wrap(err, "bid consistency validation failed")
|
||||
}
|
||||
|
||||
@@ -184,28 +184,6 @@ func signBid(t *testing.T, sk common.SecretKey, bid *ethpb.ExecutionPayloadBid,
|
||||
return out
|
||||
}
|
||||
|
||||
func blobCommitmentsForSlot(slot primitives.Slot, count int) [][]byte {
|
||||
max := int(params.BeaconConfig().MaxBlobsPerBlockAtEpoch(slots.ToEpoch(slot)))
|
||||
if count > max {
|
||||
count = max
|
||||
}
|
||||
commitments := make([][]byte, count)
|
||||
for i := range commitments {
|
||||
commitments[i] = bytes.Repeat([]byte{0xEE}, 48)
|
||||
}
|
||||
return commitments
|
||||
}
|
||||
|
||||
func tooManyBlobCommitmentsForSlot(slot primitives.Slot) [][]byte {
|
||||
max := int(params.BeaconConfig().MaxBlobsPerBlockAtEpoch(slots.ToEpoch(slot)))
|
||||
count := max + 1
|
||||
commitments := make([][]byte, count)
|
||||
for i := range commitments {
|
||||
commitments[i] = bytes.Repeat([]byte{0xEE}, 48)
|
||||
}
|
||||
return commitments
|
||||
}
|
||||
|
||||
func TestProcessExecutionPayloadBid_SelfBuildSuccess(t *testing.T) {
|
||||
slot := primitives.Slot(12)
|
||||
proposerIdx := primitives.ValidatorIndex(0)
|
||||
@@ -216,17 +194,17 @@ func TestProcessExecutionPayloadBid_SelfBuildSuccess(t *testing.T) {
|
||||
state := buildGloasState(t, slot, proposerIdx, builderIdx, params.BeaconConfig().MinActivationBalance+1000, randao, latestHash, pubKey)
|
||||
|
||||
bid := ðpb.ExecutionPayloadBid{
|
||||
ParentBlockHash: latestHash[:],
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0xCC}, 32),
|
||||
BlockHash: bytes.Repeat([]byte{0xDD}, 32),
|
||||
PrevRandao: randao[:],
|
||||
GasLimit: 1,
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
Value: 0,
|
||||
ExecutionPayment: 0,
|
||||
BlobKzgCommitments: blobCommitmentsForSlot(slot, 1),
|
||||
FeeRecipient: bytes.Repeat([]byte{0xFF}, 20),
|
||||
ParentBlockHash: latestHash[:],
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0xCC}, 32),
|
||||
BlockHash: bytes.Repeat([]byte{0xDD}, 32),
|
||||
PrevRandao: randao[:],
|
||||
GasLimit: 1,
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
Value: 0,
|
||||
ExecutionPayment: 0,
|
||||
BlobKzgCommitmentsRoot: bytes.Repeat([]byte{0xEE}, 32),
|
||||
FeeRecipient: bytes.Repeat([]byte{0xFF}, 20),
|
||||
}
|
||||
signed := ðpb.SignedExecutionPayloadBid{
|
||||
Message: bid,
|
||||
@@ -258,16 +236,16 @@ func TestProcessExecutionPayloadBid_SelfBuildNonZeroAmountFails(t *testing.T) {
|
||||
state := buildGloasState(t, slot, proposerIdx, builderIdx, params.BeaconConfig().MinActivationBalance+1000, randao, latestHash, [48]byte{})
|
||||
|
||||
bid := ðpb.ExecutionPayloadBid{
|
||||
ParentBlockHash: latestHash[:],
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0xAA}, 32),
|
||||
BlockHash: bytes.Repeat([]byte{0xBB}, 32),
|
||||
PrevRandao: randao[:],
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
Value: 10,
|
||||
ExecutionPayment: 0,
|
||||
BlobKzgCommitments: blobCommitmentsForSlot(slot, 1),
|
||||
FeeRecipient: bytes.Repeat([]byte{0xDD}, 20),
|
||||
ParentBlockHash: latestHash[:],
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0xAA}, 32),
|
||||
BlockHash: bytes.Repeat([]byte{0xBB}, 32),
|
||||
PrevRandao: randao[:],
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
Value: 10,
|
||||
ExecutionPayment: 0,
|
||||
BlobKzgCommitmentsRoot: bytes.Repeat([]byte{0xCC}, 32),
|
||||
FeeRecipient: bytes.Repeat([]byte{0xDD}, 20),
|
||||
}
|
||||
signed := ðpb.SignedExecutionPayloadBid{
|
||||
Message: bid,
|
||||
@@ -302,17 +280,17 @@ func TestProcessExecutionPayloadBid_PendingPaymentAndCacheBid(t *testing.T) {
|
||||
state := buildGloasState(t, slot, proposerIdx, builderIdx, balance, randao, latestHash, pubKey)
|
||||
|
||||
bid := ðpb.ExecutionPayloadBid{
|
||||
ParentBlockHash: latestHash[:],
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0xCC}, 32),
|
||||
BlockHash: bytes.Repeat([]byte{0xDD}, 32),
|
||||
PrevRandao: randao[:],
|
||||
GasLimit: 1,
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
Value: 500_000,
|
||||
ExecutionPayment: 1,
|
||||
BlobKzgCommitments: blobCommitmentsForSlot(slot, 1),
|
||||
FeeRecipient: bytes.Repeat([]byte{0xFF}, 20),
|
||||
ParentBlockHash: latestHash[:],
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0xCC}, 32),
|
||||
BlockHash: bytes.Repeat([]byte{0xDD}, 32),
|
||||
PrevRandao: randao[:],
|
||||
GasLimit: 1,
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
Value: 500_000,
|
||||
ExecutionPayment: 1,
|
||||
BlobKzgCommitmentsRoot: bytes.Repeat([]byte{0xEE}, 32),
|
||||
FeeRecipient: bytes.Repeat([]byte{0xFF}, 20),
|
||||
}
|
||||
|
||||
genesis := bytesutil.ToBytes32(state.GenesisValidatorsRoot())
|
||||
@@ -363,17 +341,17 @@ func TestProcessExecutionPayloadBid_BuilderNotActive(t *testing.T) {
|
||||
state = stateIface.(*state_native.BeaconState)
|
||||
|
||||
bid := ðpb.ExecutionPayloadBid{
|
||||
ParentBlockHash: latestHash[:],
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0x03}, 32),
|
||||
BlockHash: bytes.Repeat([]byte{0x04}, 32),
|
||||
PrevRandao: randao[:],
|
||||
GasLimit: 1,
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
Value: 10,
|
||||
ExecutionPayment: 0,
|
||||
BlobKzgCommitments: blobCommitmentsForSlot(slot, 1),
|
||||
FeeRecipient: bytes.Repeat([]byte{0x06}, 20),
|
||||
ParentBlockHash: latestHash[:],
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0x03}, 32),
|
||||
BlockHash: bytes.Repeat([]byte{0x04}, 32),
|
||||
PrevRandao: randao[:],
|
||||
GasLimit: 1,
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
Value: 10,
|
||||
ExecutionPayment: 0,
|
||||
BlobKzgCommitmentsRoot: bytes.Repeat([]byte{0x05}, 32),
|
||||
FeeRecipient: bytes.Repeat([]byte{0x06}, 20),
|
||||
}
|
||||
genesis := bytesutil.ToBytes32(state.GenesisValidatorsRoot())
|
||||
sig := signBid(t, sk, bid, state.Fork(), genesis)
|
||||
@@ -416,17 +394,17 @@ func TestProcessExecutionPayloadBid_CannotCoverBid(t *testing.T) {
|
||||
state = stateIface.(*state_native.BeaconState)
|
||||
|
||||
bid := ðpb.ExecutionPayloadBid{
|
||||
ParentBlockHash: latestHash[:],
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0xCC}, 32),
|
||||
BlockHash: bytes.Repeat([]byte{0xDD}, 32),
|
||||
PrevRandao: randao[:],
|
||||
GasLimit: 1,
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
Value: 25,
|
||||
ExecutionPayment: 0,
|
||||
BlobKzgCommitments: blobCommitmentsForSlot(slot, 1),
|
||||
FeeRecipient: bytes.Repeat([]byte{0xFF}, 20),
|
||||
ParentBlockHash: latestHash[:],
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0xCC}, 32),
|
||||
BlockHash: bytes.Repeat([]byte{0xDD}, 32),
|
||||
PrevRandao: randao[:],
|
||||
GasLimit: 1,
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
Value: 25,
|
||||
ExecutionPayment: 0,
|
||||
BlobKzgCommitmentsRoot: bytes.Repeat([]byte{0xEE}, 32),
|
||||
FeeRecipient: bytes.Repeat([]byte{0xFF}, 20),
|
||||
}
|
||||
genesis := bytesutil.ToBytes32(state.GenesisValidatorsRoot())
|
||||
sig := signBid(t, sk, bid, state.Fork(), genesis)
|
||||
@@ -458,17 +436,17 @@ func TestProcessExecutionPayloadBid_InvalidSignature(t *testing.T) {
|
||||
state := buildGloasState(t, slot, proposerIdx, builderIdx, params.BeaconConfig().MinDepositAmount+1000, randao, latestHash, pubKey)
|
||||
|
||||
bid := ðpb.ExecutionPayloadBid{
|
||||
ParentBlockHash: latestHash[:],
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0xCC}, 32),
|
||||
BlockHash: bytes.Repeat([]byte{0xDD}, 32),
|
||||
PrevRandao: randao[:],
|
||||
GasLimit: 1,
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
Value: 10,
|
||||
ExecutionPayment: 0,
|
||||
BlobKzgCommitments: blobCommitmentsForSlot(slot, 1),
|
||||
FeeRecipient: bytes.Repeat([]byte{0xFF}, 20),
|
||||
ParentBlockHash: latestHash[:],
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0xCC}, 32),
|
||||
BlockHash: bytes.Repeat([]byte{0xDD}, 32),
|
||||
PrevRandao: randao[:],
|
||||
GasLimit: 1,
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
Value: 10,
|
||||
ExecutionPayment: 0,
|
||||
BlobKzgCommitmentsRoot: bytes.Repeat([]byte{0xEE}, 32),
|
||||
FeeRecipient: bytes.Repeat([]byte{0xFF}, 20),
|
||||
}
|
||||
// Use an invalid signature.
|
||||
invalidSig := [96]byte{1}
|
||||
@@ -485,42 +463,6 @@ func TestProcessExecutionPayloadBid_InvalidSignature(t *testing.T) {
|
||||
require.ErrorContains(t, "bid signature validation failed", err)
|
||||
}
|
||||
|
||||
func TestProcessExecutionPayloadBid_TooManyBlobCommitments(t *testing.T) {
|
||||
slot := primitives.Slot(9)
|
||||
proposerIdx := primitives.ValidatorIndex(0)
|
||||
builderIdx := params.BeaconConfig().BuilderIndexSelfBuild
|
||||
randao := [32]byte(bytes.Repeat([]byte{0xAA}, 32))
|
||||
latestHash := [32]byte(bytes.Repeat([]byte{0xBB}, 32))
|
||||
pubKey := [48]byte{}
|
||||
state := buildGloasState(t, slot, proposerIdx, builderIdx, params.BeaconConfig().MinActivationBalance+1000, randao, latestHash, pubKey)
|
||||
|
||||
bid := ðpb.ExecutionPayloadBid{
|
||||
ParentBlockHash: latestHash[:],
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0xCC}, 32),
|
||||
BlockHash: bytes.Repeat([]byte{0xDD}, 32),
|
||||
PrevRandao: randao[:],
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
BlobKzgCommitments: tooManyBlobCommitmentsForSlot(slot),
|
||||
FeeRecipient: bytes.Repeat([]byte{0xFF}, 20),
|
||||
}
|
||||
signed := ðpb.SignedExecutionPayloadBid{
|
||||
Message: bid,
|
||||
Signature: common.InfiniteSignature[:],
|
||||
}
|
||||
|
||||
block := stubBlock{
|
||||
slot: slot,
|
||||
proposer: proposerIdx,
|
||||
parentRoot: bytesutil.ToBytes32(bid.ParentBlockRoot),
|
||||
body: stubBlockBody{signedBid: signed},
|
||||
v: version.Gloas,
|
||||
}
|
||||
|
||||
err := ProcessExecutionPayloadBid(state, block)
|
||||
require.ErrorContains(t, "blob KZG commitments over max", err)
|
||||
}
|
||||
|
||||
func TestProcessExecutionPayloadBid_SlotMismatch(t *testing.T) {
|
||||
slot := primitives.Slot(10)
|
||||
builderIdx := primitives.BuilderIndex(1)
|
||||
@@ -536,17 +478,17 @@ func TestProcessExecutionPayloadBid_SlotMismatch(t *testing.T) {
|
||||
state := buildGloasState(t, slot, proposerIdx, builderIdx, params.BeaconConfig().MinDepositAmount+1000, randao, latestHash, pubKey)
|
||||
|
||||
bid := ðpb.ExecutionPayloadBid{
|
||||
ParentBlockHash: latestHash[:],
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0xAA}, 32),
|
||||
BlockHash: bytes.Repeat([]byte{0xBB}, 32),
|
||||
PrevRandao: randao[:],
|
||||
GasLimit: 1,
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot + 1, // mismatch
|
||||
Value: 1,
|
||||
ExecutionPayment: 0,
|
||||
BlobKzgCommitments: blobCommitmentsForSlot(slot, 1),
|
||||
FeeRecipient: bytes.Repeat([]byte{0xDD}, 20),
|
||||
ParentBlockHash: latestHash[:],
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0xAA}, 32),
|
||||
BlockHash: bytes.Repeat([]byte{0xBB}, 32),
|
||||
PrevRandao: randao[:],
|
||||
GasLimit: 1,
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot + 1, // mismatch
|
||||
Value: 1,
|
||||
ExecutionPayment: 0,
|
||||
BlobKzgCommitmentsRoot: bytes.Repeat([]byte{0xCC}, 32),
|
||||
FeeRecipient: bytes.Repeat([]byte{0xDD}, 20),
|
||||
}
|
||||
genesis := bytesutil.ToBytes32(state.GenesisValidatorsRoot())
|
||||
sig := signBid(t, sk, bid, state.Fork(), genesis)
|
||||
@@ -578,17 +520,17 @@ func TestProcessExecutionPayloadBid_ParentHashMismatch(t *testing.T) {
|
||||
state := buildGloasState(t, slot, proposerIdx, builderIdx, params.BeaconConfig().MinDepositAmount+1000, randao, latestHash, pubKey)
|
||||
|
||||
bid := ðpb.ExecutionPayloadBid{
|
||||
ParentBlockHash: bytes.Repeat([]byte{0x11}, 32), // mismatch
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0x22}, 32),
|
||||
BlockHash: bytes.Repeat([]byte{0x33}, 32),
|
||||
PrevRandao: randao[:],
|
||||
GasLimit: 1,
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
Value: 1,
|
||||
ExecutionPayment: 0,
|
||||
BlobKzgCommitments: blobCommitmentsForSlot(slot, 1),
|
||||
FeeRecipient: bytes.Repeat([]byte{0x55}, 20),
|
||||
ParentBlockHash: bytes.Repeat([]byte{0x11}, 32), // mismatch
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0x22}, 32),
|
||||
BlockHash: bytes.Repeat([]byte{0x33}, 32),
|
||||
PrevRandao: randao[:],
|
||||
GasLimit: 1,
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
Value: 1,
|
||||
ExecutionPayment: 0,
|
||||
BlobKzgCommitmentsRoot: bytes.Repeat([]byte{0x44}, 32),
|
||||
FeeRecipient: bytes.Repeat([]byte{0x55}, 20),
|
||||
}
|
||||
genesis := bytesutil.ToBytes32(state.GenesisValidatorsRoot())
|
||||
sig := signBid(t, sk, bid, state.Fork(), genesis)
|
||||
@@ -621,17 +563,17 @@ func TestProcessExecutionPayloadBid_ParentRootMismatch(t *testing.T) {
|
||||
|
||||
parentRoot := bytes.Repeat([]byte{0x22}, 32)
|
||||
bid := ðpb.ExecutionPayloadBid{
|
||||
ParentBlockHash: latestHash[:],
|
||||
ParentBlockRoot: parentRoot,
|
||||
BlockHash: bytes.Repeat([]byte{0x33}, 32),
|
||||
PrevRandao: randao[:],
|
||||
GasLimit: 1,
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
Value: 1,
|
||||
ExecutionPayment: 0,
|
||||
BlobKzgCommitments: blobCommitmentsForSlot(slot, 1),
|
||||
FeeRecipient: bytes.Repeat([]byte{0x55}, 20),
|
||||
ParentBlockHash: latestHash[:],
|
||||
ParentBlockRoot: parentRoot,
|
||||
BlockHash: bytes.Repeat([]byte{0x33}, 32),
|
||||
PrevRandao: randao[:],
|
||||
GasLimit: 1,
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
Value: 1,
|
||||
ExecutionPayment: 0,
|
||||
BlobKzgCommitmentsRoot: bytes.Repeat([]byte{0x44}, 32),
|
||||
FeeRecipient: bytes.Repeat([]byte{0x55}, 20),
|
||||
}
|
||||
genesis := bytesutil.ToBytes32(state.GenesisValidatorsRoot())
|
||||
sig := signBid(t, sk, bid, state.Fork(), genesis)
|
||||
@@ -663,17 +605,17 @@ func TestProcessExecutionPayloadBid_PrevRandaoMismatch(t *testing.T) {
|
||||
state := buildGloasState(t, slot, proposerIdx, builderIdx, params.BeaconConfig().MinDepositAmount+1000, randao, latestHash, pubKey)
|
||||
|
||||
bid := ðpb.ExecutionPayloadBid{
|
||||
ParentBlockHash: latestHash[:],
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0x22}, 32),
|
||||
BlockHash: bytes.Repeat([]byte{0x33}, 32),
|
||||
PrevRandao: bytes.Repeat([]byte{0x01}, 32), // mismatch
|
||||
GasLimit: 1,
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
Value: 1,
|
||||
ExecutionPayment: 0,
|
||||
BlobKzgCommitments: blobCommitmentsForSlot(slot, 1),
|
||||
FeeRecipient: bytes.Repeat([]byte{0x55}, 20),
|
||||
ParentBlockHash: latestHash[:],
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0x22}, 32),
|
||||
BlockHash: bytes.Repeat([]byte{0x33}, 32),
|
||||
PrevRandao: bytes.Repeat([]byte{0x01}, 32), // mismatch
|
||||
GasLimit: 1,
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
Value: 1,
|
||||
ExecutionPayment: 0,
|
||||
BlobKzgCommitmentsRoot: bytes.Repeat([]byte{0x44}, 32),
|
||||
FeeRecipient: bytes.Repeat([]byte{0x55}, 20),
|
||||
}
|
||||
genesis := bytesutil.ToBytes32(state.GenesisValidatorsRoot())
|
||||
sig := signBid(t, sk, bid, state.Fork(), genesis)
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
package gloas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
|
||||
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
|
||||
"github.com/OffchainLabs/prysm/v7/config/params"
|
||||
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
|
||||
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
|
||||
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
"github.com/OffchainLabs/prysm/v7/runtime/version"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func processDepositRequests(ctx context.Context, beaconState state.BeaconState, requests []*enginev1.DepositRequest) error {
|
||||
if len(requests) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, receipt := range requests {
|
||||
if err := processDepositRequest(beaconState, receipt); err != nil {
|
||||
return errors.Wrap(err, "could not apply deposit request")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// processDepositRequest processes the specific deposit request
|
||||
//
|
||||
// <spec fn="process_deposit_request" fork="gloas" hash="3c6b0310">
|
||||
// def process_deposit_request(state: BeaconState, deposit_request: DepositRequest) -> None:
|
||||
// # [New in Gloas:EIP7732]
|
||||
// builder_pubkeys = [b.pubkey for b in state.builders]
|
||||
// validator_pubkeys = [v.pubkey for v in state.validators]
|
||||
//
|
||||
// # [New in Gloas:EIP7732]
|
||||
// # Regardless of the withdrawal credentials prefix, if a builder/validator
|
||||
// # already exists with this pubkey, apply the deposit to their balance
|
||||
// is_builder = deposit_request.pubkey in builder_pubkeys
|
||||
// is_validator = deposit_request.pubkey in validator_pubkeys
|
||||
// is_builder_prefix = is_builder_withdrawal_credential(deposit_request.withdrawal_credentials)
|
||||
// if is_builder or (is_builder_prefix and not is_validator):
|
||||
// # Apply builder deposits immediately
|
||||
// apply_deposit_for_builder(
|
||||
// state,
|
||||
// deposit_request.pubkey,
|
||||
// deposit_request.withdrawal_credentials,
|
||||
// deposit_request.amount,
|
||||
// deposit_request.signature,
|
||||
// state.slot,
|
||||
// )
|
||||
// return
|
||||
//
|
||||
// # Add validator deposits to the queue
|
||||
// state.pending_deposits.append(
|
||||
// PendingDeposit(
|
||||
// pubkey=deposit_request.pubkey,
|
||||
// withdrawal_credentials=deposit_request.withdrawal_credentials,
|
||||
// amount=deposit_request.amount,
|
||||
// signature=deposit_request.signature,
|
||||
// slot=state.slot,
|
||||
// )
|
||||
// )
|
||||
// </spec>
|
||||
func processDepositRequest(beaconState state.BeaconState, request *enginev1.DepositRequest) error {
|
||||
if request == nil {
|
||||
return errors.New("nil deposit request")
|
||||
}
|
||||
|
||||
applied, err := applyBuilderDepositRequest(beaconState, request)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not apply builder deposit")
|
||||
}
|
||||
if applied {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := beaconState.AppendPendingDeposit(ðpb.PendingDeposit{
|
||||
PublicKey: request.Pubkey,
|
||||
WithdrawalCredentials: request.WithdrawalCredentials,
|
||||
Amount: request.Amount,
|
||||
Signature: request.Signature,
|
||||
Slot: beaconState.Slot(),
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "could not append deposit request")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// <spec fn="apply_deposit_for_builder" fork="gloas" hash="e4bc98c7">
|
||||
// def apply_deposit_for_builder(
|
||||
//
|
||||
// state: BeaconState,
|
||||
// pubkey: BLSPubkey,
|
||||
// withdrawal_credentials: Bytes32,
|
||||
// amount: uint64,
|
||||
// signature: BLSSignature,
|
||||
// slot: Slot,
|
||||
//
|
||||
// ) -> None:
|
||||
//
|
||||
// builder_pubkeys = [b.pubkey for b in state.builders]
|
||||
// if pubkey not in builder_pubkeys:
|
||||
// # Verify the deposit signature (proof of possession) which is not checked by the deposit contract
|
||||
// if is_valid_deposit_signature(pubkey, withdrawal_credentials, amount, signature):
|
||||
// add_builder_to_registry(state, pubkey, withdrawal_credentials, amount, slot)
|
||||
// else:
|
||||
// # Increase balance by deposit amount
|
||||
// builder_index = builder_pubkeys.index(pubkey)
|
||||
// state.builders[builder_index].balance += amount
|
||||
//
|
||||
// </spec>
|
||||
func applyBuilderDepositRequest(beaconState state.BeaconState, request *enginev1.DepositRequest) (bool, error) {
|
||||
if beaconState.Version() < version.Gloas {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
pubkey := bytesutil.ToBytes48(request.Pubkey)
|
||||
_, isValidator := beaconState.ValidatorIndexByPubkey(pubkey)
|
||||
idx, isBuilder := beaconState.BuilderIndexByPubkey(pubkey)
|
||||
isBuilderPrefix := IsBuilderWithdrawalCredential(request.WithdrawalCredentials)
|
||||
if !isBuilder && (!isBuilderPrefix || isValidator) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if isBuilder {
|
||||
if err := beaconState.IncreaseBuilderBalance(idx, request.Amount); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if err := applyDepositForNewBuilder(
|
||||
beaconState,
|
||||
request.Pubkey,
|
||||
request.WithdrawalCredentials,
|
||||
request.Amount,
|
||||
request.Signature,
|
||||
); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func applyDepositForNewBuilder(
|
||||
beaconState state.BeaconState,
|
||||
pubkey []byte,
|
||||
withdrawalCredentials []byte,
|
||||
amount uint64,
|
||||
signature []byte,
|
||||
) error {
|
||||
pubkeyBytes := bytesutil.ToBytes48(pubkey)
|
||||
valid, err := helpers.IsValidDepositSignature(ðpb.Deposit_Data{
|
||||
PublicKey: pubkey,
|
||||
WithdrawalCredentials: withdrawalCredentials,
|
||||
Amount: amount,
|
||||
Signature: signature,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not verify deposit signature")
|
||||
}
|
||||
if !valid {
|
||||
log.WithFields(logrus.Fields{
|
||||
"pubkey": fmt.Sprintf("%x", pubkey),
|
||||
}).Warn("ignoring builder deposit: invalid signature")
|
||||
return nil
|
||||
}
|
||||
|
||||
withdrawalCredBytes := bytesutil.ToBytes32(withdrawalCredentials)
|
||||
return beaconState.AddBuilderFromDeposit(pubkeyBytes, withdrawalCredBytes, amount)
|
||||
}
|
||||
|
||||
func IsBuilderWithdrawalCredential(withdrawalCredentials []byte) bool {
|
||||
return len(withdrawalCredentials) == fieldparams.RootLength &&
|
||||
withdrawalCredentials[0] == params.BeaconConfig().BuilderWithdrawalPrefixByte
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package gloas
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
|
||||
state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native"
|
||||
stateTesting "github.com/OffchainLabs/prysm/v7/beacon-chain/state/testing"
|
||||
"github.com/OffchainLabs/prysm/v7/config/params"
|
||||
"github.com/OffchainLabs/prysm/v7/crypto/bls"
|
||||
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
|
||||
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/require"
|
||||
)
|
||||
|
||||
func TestProcessDepositRequests_EmptyAndNil(t *testing.T) {
|
||||
st := newGloasState(t, nil, nil)
|
||||
|
||||
t.Run("empty requests continues", func(t *testing.T) {
|
||||
err := processDepositRequests(t.Context(), st, []*enginev1.DepositRequest{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("nil request errors", func(t *testing.T) {
|
||||
err := processDepositRequests(t.Context(), st, []*enginev1.DepositRequest{nil})
|
||||
require.ErrorContains(t, "nil deposit request", err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcessDepositRequest_BuilderDepositAddsBuilder(t *testing.T) {
|
||||
sk, err := bls.RandKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
cred := builderWithdrawalCredentials()
|
||||
pd := stateTesting.GeneratePendingDeposit(t, sk, 1234, cred, 0)
|
||||
req := depositRequestFromPending(pd, 1)
|
||||
|
||||
st := newGloasState(t, nil, nil)
|
||||
err = processDepositRequest(st, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
idx, ok := st.BuilderIndexByPubkey(toBytes48(req.Pubkey))
|
||||
require.Equal(t, true, ok)
|
||||
|
||||
builder, err := st.Builder(idx)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, builder)
|
||||
require.DeepEqual(t, req.Pubkey, builder.Pubkey)
|
||||
require.DeepEqual(t, []byte{cred[0]}, builder.Version)
|
||||
require.DeepEqual(t, cred[12:], builder.ExecutionAddress)
|
||||
require.Equal(t, uint64(1234), uint64(builder.Balance))
|
||||
require.Equal(t, params.BeaconConfig().FarFutureEpoch, builder.WithdrawableEpoch)
|
||||
|
||||
pending, err := st.PendingDeposits()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, len(pending))
|
||||
}
|
||||
|
||||
func TestProcessDepositRequest_ExistingBuilderIncreasesBalance(t *testing.T) {
|
||||
sk, err := bls.RandKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
pubkey := sk.PublicKey().Marshal()
|
||||
builders := []*ethpb.Builder{
|
||||
{
|
||||
Pubkey: pubkey,
|
||||
Version: []byte{0},
|
||||
ExecutionAddress: bytes.Repeat([]byte{0x11}, 20),
|
||||
Balance: 5,
|
||||
WithdrawableEpoch: params.BeaconConfig().FarFutureEpoch,
|
||||
},
|
||||
}
|
||||
st := newGloasState(t, nil, builders)
|
||||
|
||||
cred := validatorWithdrawalCredentials()
|
||||
pd := stateTesting.GeneratePendingDeposit(t, sk, 200, cred, 0)
|
||||
req := depositRequestFromPending(pd, 9)
|
||||
|
||||
err = processDepositRequest(st, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
idx, ok := st.BuilderIndexByPubkey(toBytes48(pubkey))
|
||||
require.Equal(t, true, ok)
|
||||
builder, err := st.Builder(idx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(205), uint64(builder.Balance))
|
||||
|
||||
pending, err := st.PendingDeposits()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, len(pending))
|
||||
}
|
||||
|
||||
func TestApplyDepositForBuilder_InvalidSignatureIgnoresDeposit(t *testing.T) {
|
||||
sk, err := bls.RandKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
cred := builderWithdrawalCredentials()
|
||||
st := newGloasState(t, nil, nil)
|
||||
err = applyDepositForNewBuilder(st, sk.PublicKey().Marshal(), cred[:], 100, make([]byte, 96))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, ok := st.BuilderIndexByPubkey(toBytes48(sk.PublicKey().Marshal()))
|
||||
require.Equal(t, false, ok)
|
||||
}
|
||||
|
||||
func newGloasState(t *testing.T, validators []*ethpb.Validator, builders []*ethpb.Builder) state.BeaconState {
|
||||
t.Helper()
|
||||
|
||||
st, err := state_native.InitializeFromProtoGloas(ðpb.BeaconStateGloas{
|
||||
DepositRequestsStartIndex: params.BeaconConfig().UnsetDepositRequestsStartIndex,
|
||||
Validators: validators,
|
||||
Balances: make([]uint64, len(validators)),
|
||||
PendingDeposits: []*ethpb.PendingDeposit{},
|
||||
Builders: builders,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return st
|
||||
}
|
||||
|
||||
func depositRequestFromPending(pd *ethpb.PendingDeposit, index uint64) *enginev1.DepositRequest {
|
||||
return &enginev1.DepositRequest{
|
||||
Pubkey: pd.PublicKey,
|
||||
WithdrawalCredentials: pd.WithdrawalCredentials,
|
||||
Amount: pd.Amount,
|
||||
Signature: pd.Signature,
|
||||
Index: index,
|
||||
}
|
||||
}
|
||||
|
||||
func builderWithdrawalCredentials() [32]byte {
|
||||
var cred [32]byte
|
||||
cred[0] = params.BeaconConfig().BuilderWithdrawalPrefixByte
|
||||
copy(cred[12:], bytes.Repeat([]byte{0x22}, 20))
|
||||
return cred
|
||||
}
|
||||
|
||||
func validatorWithdrawalCredentials() [32]byte {
|
||||
var cred [32]byte
|
||||
cred[0] = params.BeaconConfig().ETH1AddressWithdrawalPrefixByte
|
||||
copy(cred[12:], bytes.Repeat([]byte{0x33}, 20))
|
||||
return cred
|
||||
}
|
||||
|
||||
func toBytes48(b []byte) [48]byte {
|
||||
var out [48]byte
|
||||
copy(out[:], b)
|
||||
return out
|
||||
}
|
||||
@@ -1,345 +0,0 @@
|
||||
package gloas
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
requests "github.com/OffchainLabs/prysm/v7/beacon-chain/core/requests"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
|
||||
"github.com/OffchainLabs/prysm/v7/config/params"
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
||||
"github.com/OffchainLabs/prysm/v7/crypto/bls"
|
||||
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
|
||||
"github.com/OffchainLabs/prysm/v7/time/slots"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ProcessExecutionPayload processes the signed execution payload envelope for the Gloas fork.
|
||||
//
|
||||
// <spec fn="process_execution_payload" fork="gloas" hash="36bd3af3">
|
||||
// def process_execution_payload(
|
||||
// state: BeaconState,
|
||||
// # [Modified in Gloas:EIP7732]
|
||||
// # Removed `body`
|
||||
// # [New in Gloas:EIP7732]
|
||||
// signed_envelope: SignedExecutionPayloadEnvelope,
|
||||
// execution_engine: ExecutionEngine,
|
||||
// # [New in Gloas:EIP7732]
|
||||
// verify: bool = True,
|
||||
// ) -> None:
|
||||
// envelope = signed_envelope.message
|
||||
// payload = envelope.payload
|
||||
//
|
||||
// # Verify signature
|
||||
// if verify:
|
||||
// assert verify_execution_payload_envelope_signature(state, signed_envelope)
|
||||
//
|
||||
// # Cache latest block header state root
|
||||
// previous_state_root = hash_tree_root(state)
|
||||
// if state.latest_block_header.state_root == Root():
|
||||
// state.latest_block_header.state_root = previous_state_root
|
||||
//
|
||||
// # Verify consistency with the beacon block
|
||||
// assert envelope.beacon_block_root == hash_tree_root(state.latest_block_header)
|
||||
// assert envelope.slot == state.slot
|
||||
//
|
||||
// # Verify consistency with the committed bid
|
||||
// committed_bid = state.latest_execution_payload_bid
|
||||
// assert envelope.builder_index == committed_bid.builder_index
|
||||
// assert committed_bid.prev_randao == payload.prev_randao
|
||||
//
|
||||
// # Verify consistency with expected withdrawals
|
||||
// assert hash_tree_root(payload.withdrawals) == hash_tree_root(state.payload_expected_withdrawals)
|
||||
//
|
||||
// # Verify the gas_limit
|
||||
// assert committed_bid.gas_limit == payload.gas_limit
|
||||
// # Verify the block hash
|
||||
// assert committed_bid.block_hash == payload.block_hash
|
||||
// # Verify consistency of the parent hash with respect to the previous execution payload
|
||||
// assert payload.parent_hash == state.latest_block_hash
|
||||
// # Verify timestamp
|
||||
// assert payload.timestamp == compute_time_at_slot(state, state.slot)
|
||||
// # Verify the execution payload is valid
|
||||
// versioned_hashes = [
|
||||
// kzg_commitment_to_versioned_hash(commitment)
|
||||
// # [Modified in Gloas:EIP7732]
|
||||
// for commitment in committed_bid.blob_kzg_commitments
|
||||
// ]
|
||||
// requests = envelope.execution_requests
|
||||
// assert execution_engine.verify_and_notify_new_payload(
|
||||
// NewPayloadRequest(
|
||||
// execution_payload=payload,
|
||||
// versioned_hashes=versioned_hashes,
|
||||
// parent_beacon_block_root=state.latest_block_header.parent_root,
|
||||
// execution_requests=requests,
|
||||
// )
|
||||
// )
|
||||
//
|
||||
// def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None:
|
||||
// for operation in operations:
|
||||
// fn(state, operation)
|
||||
//
|
||||
// for_ops(requests.deposits, process_deposit_request)
|
||||
// for_ops(requests.withdrawals, process_withdrawal_request)
|
||||
// for_ops(requests.consolidations, process_consolidation_request)
|
||||
//
|
||||
// # Queue the builder payment
|
||||
// payment = state.builder_pending_payments[SLOTS_PER_EPOCH + state.slot % SLOTS_PER_EPOCH]
|
||||
// amount = payment.withdrawal.amount
|
||||
// if amount > 0:
|
||||
// state.builder_pending_withdrawals.append(payment.withdrawal)
|
||||
// state.builder_pending_payments[SLOTS_PER_EPOCH + state.slot % SLOTS_PER_EPOCH] = (
|
||||
// BuilderPendingPayment()
|
||||
// )
|
||||
//
|
||||
// # Cache the execution payload hash
|
||||
// state.execution_payload_availability[state.slot % SLOTS_PER_HISTORICAL_ROOT] = 0b1
|
||||
// state.latest_block_hash = payload.block_hash
|
||||
//
|
||||
// # Verify the state root
|
||||
// if verify:
|
||||
// assert envelope.state_root == hash_tree_root(state)
|
||||
// </spec>
|
||||
func ProcessExecutionPayload(
|
||||
ctx context.Context,
|
||||
st state.BeaconState,
|
||||
signedEnvelope interfaces.ROSignedExecutionPayloadEnvelope,
|
||||
) error {
|
||||
if err := verifyExecutionPayloadEnvelopeSignature(st, signedEnvelope); err != nil {
|
||||
return errors.Wrap(err, "signature verification failed")
|
||||
}
|
||||
|
||||
latestHeader := st.LatestBlockHeader()
|
||||
if len(latestHeader.StateRoot) == 0 || bytes.Equal(latestHeader.StateRoot, make([]byte, 32)) {
|
||||
previousStateRoot, err := st.HashTreeRoot(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not compute state root")
|
||||
}
|
||||
latestHeader.StateRoot = previousStateRoot[:]
|
||||
if err := st.SetLatestBlockHeader(latestHeader); err != nil {
|
||||
return errors.Wrap(err, "could not set latest block header")
|
||||
}
|
||||
}
|
||||
|
||||
blockHeaderRoot, err := latestHeader.HashTreeRoot()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not compute block header root")
|
||||
}
|
||||
envelope, err := signedEnvelope.Envelope()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not get envelope from signed envelope")
|
||||
}
|
||||
|
||||
beaconBlockRoot := envelope.BeaconBlockRoot()
|
||||
if !bytes.Equal(beaconBlockRoot[:], blockHeaderRoot[:]) {
|
||||
return errors.Errorf("envelope beacon block root does not match state latest block header root: envelope=%#x, header=%#x", beaconBlockRoot, blockHeaderRoot)
|
||||
}
|
||||
|
||||
if envelope.Slot() != st.Slot() {
|
||||
return errors.Errorf("envelope slot does not match state slot: envelope=%d, state=%d", envelope.Slot(), st.Slot())
|
||||
}
|
||||
|
||||
latestBid, err := st.LatestExecutionPayloadBid()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not get latest execution payload bid")
|
||||
}
|
||||
if latestBid == nil {
|
||||
return errors.New("latest execution payload bid is nil")
|
||||
}
|
||||
if envelope.BuilderIndex() != latestBid.BuilderIndex() {
|
||||
return errors.Errorf("envelope builder index does not match committed bid builder index: envelope=%d, bid=%d", envelope.BuilderIndex(), latestBid.BuilderIndex())
|
||||
}
|
||||
|
||||
payload, err := envelope.Execution()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not get execution payload from envelope")
|
||||
}
|
||||
latestBidPrevRandao := latestBid.PrevRandao()
|
||||
if !bytes.Equal(payload.PrevRandao(), latestBidPrevRandao[:]) {
|
||||
return errors.Errorf("payload prev randao does not match committed bid prev randao: payload=%#x, bid=%#x", payload.PrevRandao(), latestBidPrevRandao)
|
||||
}
|
||||
|
||||
withdrawals, err := payload.Withdrawals()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not get withdrawals from payload")
|
||||
}
|
||||
|
||||
ok, err := st.WithdrawalsMatchPayloadExpected(withdrawals)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not validate payload withdrawals")
|
||||
}
|
||||
if !ok {
|
||||
return errors.New("payload withdrawals do not match expected withdrawals")
|
||||
}
|
||||
|
||||
if latestBid.GasLimit() != payload.GasLimit() {
|
||||
return errors.Errorf("committed bid gas limit does not match payload gas limit: bid=%d, payload=%d", latestBid.GasLimit(), payload.GasLimit())
|
||||
}
|
||||
|
||||
bidBlockHash := latestBid.BlockHash()
|
||||
payloadBlockHash := payload.BlockHash()
|
||||
if !bytes.Equal(bidBlockHash[:], payloadBlockHash) {
|
||||
return errors.Errorf("committed bid block hash does not match payload block hash: bid=%#x, payload=%#x", bidBlockHash, payloadBlockHash)
|
||||
}
|
||||
|
||||
latestBlockHash, err := st.LatestBlockHash()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not get latest block hash")
|
||||
}
|
||||
if !bytes.Equal(payload.ParentHash(), latestBlockHash[:]) {
|
||||
return errors.Errorf("payload parent hash does not match state latest block hash: payload=%#x, state=%#x", payload.ParentHash(), latestBlockHash)
|
||||
}
|
||||
|
||||
t, err := slots.StartTime(st.GenesisTime(), st.Slot())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not compute timestamp")
|
||||
}
|
||||
if payload.Timestamp() != uint64(t.Unix()) {
|
||||
return errors.Errorf("payload timestamp does not match expected timestamp: payload=%d, expected=%d", payload.Timestamp(), uint64(t.Unix()))
|
||||
}
|
||||
|
||||
if err := processExecutionRequests(ctx, st, envelope.ExecutionRequests()); err != nil {
|
||||
return errors.Wrap(err, "could not process execution requests")
|
||||
}
|
||||
|
||||
if err := st.QueueBuilderPayment(); err != nil {
|
||||
return errors.Wrap(err, "could not queue builder payment")
|
||||
}
|
||||
|
||||
if err := st.SetExecutionPayloadAvailability(st.Slot(), true); err != nil {
|
||||
return errors.Wrap(err, "could not set execution payload availability")
|
||||
}
|
||||
|
||||
if err := st.SetLatestBlockHash([32]byte(payload.BlockHash())); err != nil {
|
||||
return errors.Wrap(err, "could not set latest block hash")
|
||||
}
|
||||
|
||||
r, err := st.HashTreeRoot(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not get hash tree root")
|
||||
}
|
||||
if r != envelope.StateRoot() {
|
||||
return fmt.Errorf("state root mismatch: expected %#x, got %#x", envelope.StateRoot(), r)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func envelopePublicKey(st state.BeaconState, builderIdx primitives.BuilderIndex) (bls.PublicKey, error) {
|
||||
if builderIdx == params.BeaconConfig().BuilderIndexSelfBuild {
|
||||
return proposerPublicKey(st)
|
||||
}
|
||||
return builderPublicKey(st, builderIdx)
|
||||
}
|
||||
|
||||
func proposerPublicKey(st state.BeaconState) (bls.PublicKey, error) {
|
||||
header := st.LatestBlockHeader()
|
||||
if header == nil {
|
||||
return nil, fmt.Errorf("latest block header is nil")
|
||||
}
|
||||
proposerPubkey := st.PubkeyAtIndex(header.ProposerIndex)
|
||||
publicKey, err := bls.PublicKeyFromBytes(proposerPubkey[:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid proposer public key: %w", err)
|
||||
}
|
||||
return publicKey, nil
|
||||
}
|
||||
|
||||
func builderPublicKey(st state.BeaconState, builderIdx primitives.BuilderIndex) (bls.PublicKey, error) {
|
||||
builder, err := st.Builder(builderIdx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get builder: %w", err)
|
||||
}
|
||||
if builder == nil {
|
||||
return nil, fmt.Errorf("builder at index %d not found", builderIdx)
|
||||
}
|
||||
publicKey, err := bls.PublicKeyFromBytes(builder.Pubkey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid builder public key: %w", err)
|
||||
}
|
||||
return publicKey, nil
|
||||
}
|
||||
|
||||
// processExecutionRequests processes deposits, withdrawals, and consolidations from execution requests.
|
||||
// Spec v1.7.0-alpha.0 (pseudocode):
|
||||
// for op in requests.deposits: process_deposit_request(state, op)
|
||||
// for op in requests.withdrawals: process_withdrawal_request(state, op)
|
||||
// for op in requests.consolidations: process_consolidation_request(state, op)
|
||||
func processExecutionRequests(ctx context.Context, st state.BeaconState, rqs *enginev1.ExecutionRequests) error {
|
||||
if err := processDepositRequests(ctx, st, rqs.Deposits); err != nil {
|
||||
return errors.Wrap(err, "could not process deposit requests")
|
||||
}
|
||||
|
||||
var err error
|
||||
st, err = requests.ProcessWithdrawalRequests(ctx, st, rqs.Withdrawals)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not process withdrawal requests")
|
||||
}
|
||||
err = requests.ProcessConsolidationRequests(ctx, st, rqs.Consolidations)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not process consolidation requests")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifyExecutionPayloadEnvelopeSignature verifies the BLS signature on a signed execution payload envelope.
|
||||
// Spec v1.7.0-alpha.0 (pseudocode):
|
||||
// builder_index = signed_envelope.message.builder_index
|
||||
// if builder_index == BUILDER_INDEX_SELF_BUILD:
|
||||
//
|
||||
// validator_index = state.latest_block_header.proposer_index
|
||||
// pubkey = state.validators[validator_index].pubkey
|
||||
//
|
||||
// else:
|
||||
//
|
||||
// pubkey = state.builders[builder_index].pubkey
|
||||
//
|
||||
// signing_root = compute_signing_root(
|
||||
//
|
||||
// signed_envelope.message, get_domain(state, DOMAIN_BEACON_BUILDER)
|
||||
//
|
||||
// )
|
||||
// return bls.Verify(pubkey, signing_root, signed_envelope.signature)
|
||||
func verifyExecutionPayloadEnvelopeSignature(st state.BeaconState, signedEnvelope interfaces.ROSignedExecutionPayloadEnvelope) error {
|
||||
envelope, err := signedEnvelope.Envelope()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get envelope: %w", err)
|
||||
}
|
||||
|
||||
builderIdx := envelope.BuilderIndex()
|
||||
publicKey, err := envelopePublicKey(st, builderIdx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signatureBytes := signedEnvelope.Signature()
|
||||
signature, err := bls.SignatureFromBytes(signatureBytes[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid signature format: %w", err)
|
||||
}
|
||||
|
||||
currentEpoch := slots.ToEpoch(envelope.Slot())
|
||||
domain, err := signing.Domain(
|
||||
st.Fork(),
|
||||
currentEpoch,
|
||||
params.BeaconConfig().DomainBeaconBuilder,
|
||||
st.GenesisValidatorsRoot(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compute signing domain: %w", err)
|
||||
}
|
||||
|
||||
signingRoot, err := signedEnvelope.SigningRoot(domain)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compute signing root: %w", err)
|
||||
}
|
||||
|
||||
if !signature.Verify(publicKey, signingRoot[:]) {
|
||||
return fmt.Errorf("signature verification failed: %w", signing.ErrSigFailedToVerify)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -24,21 +24,14 @@ import (
|
||||
)
|
||||
|
||||
// ProcessPayloadAttestations validates payload attestations in a block body.
|
||||
// Spec v1.7.0-alpha.0 (pseudocode):
|
||||
// process_payload_attestation(state: BeaconState, payload_attestation: PayloadAttestation):
|
||||
//
|
||||
// <spec fn="process_payload_attestation" fork="gloas" hash="f46bf0b0">
|
||||
// def process_payload_attestation(
|
||||
// state: BeaconState, payload_attestation: PayloadAttestation
|
||||
// ) -> None:
|
||||
// data = payload_attestation.data
|
||||
//
|
||||
// # Check that the attestation is for the parent beacon block
|
||||
// assert data.beacon_block_root == state.latest_block_header.parent_root
|
||||
// # Check that the attestation is for the previous slot
|
||||
// assert data.slot + 1 == state.slot
|
||||
// # Verify signature
|
||||
// indexed_payload_attestation = get_indexed_payload_attestation(state, payload_attestation)
|
||||
// assert is_valid_indexed_payload_attestation(state, indexed_payload_attestation)
|
||||
// </spec>
|
||||
// data = payload_attestation.data
|
||||
// assert data.beacon_block_root == state.latest_block_header.parent_root
|
||||
// assert data.slot + 1 == state.slot
|
||||
// indexed = get_indexed_payload_attestation(state, data.slot, payload_attestation)
|
||||
// assert is_valid_indexed_payload_attestation(state, indexed)
|
||||
func ProcessPayloadAttestations(ctx context.Context, st state.BeaconState, body interfaces.ReadOnlyBeaconBlockBody) error {
|
||||
atts, err := body.PayloadAttestations()
|
||||
if err != nil {
|
||||
@@ -77,7 +70,7 @@ func ProcessPayloadAttestations(ctx context.Context, st state.BeaconState, body
|
||||
|
||||
// indexedPayloadAttestation converts a payload attestation into its indexed form.
|
||||
func indexedPayloadAttestation(ctx context.Context, st state.ReadOnlyBeaconState, att *eth.PayloadAttestation) (*consensus_types.IndexedPayloadAttestation, error) {
|
||||
committee, err := PayloadCommittee(ctx, st, att.Data.Slot)
|
||||
committee, err := payloadCommittee(ctx, st, att.Data.Slot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -96,26 +89,19 @@ func indexedPayloadAttestation(ctx context.Context, st state.ReadOnlyBeaconState
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PayloadCommittee returns the payload timeliness committee for a given slot for the state.
|
||||
// payloadCommittee returns the payload timeliness committee for a given slot for the state.
|
||||
// Spec v1.7.0-alpha.0 (pseudocode):
|
||||
// get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]:
|
||||
//
|
||||
// <spec fn="get_ptc" fork="gloas" hash="ae15f761">
|
||||
// def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]:
|
||||
// """
|
||||
// Get the payload timeliness committee for the given ``slot``.
|
||||
// """
|
||||
// epoch = compute_epoch_at_slot(slot)
|
||||
// seed = hash(get_seed(state, epoch, DOMAIN_PTC_ATTESTER) + uint_to_bytes(slot))
|
||||
// indices: List[ValidatorIndex] = []
|
||||
// # Concatenate all committees for this slot in order
|
||||
// committees_per_slot = get_committee_count_per_slot(state, epoch)
|
||||
// for i in range(committees_per_slot):
|
||||
// committee = get_beacon_committee(state, slot, CommitteeIndex(i))
|
||||
// indices.extend(committee)
|
||||
// return compute_balance_weighted_selection(
|
||||
// state, indices, seed, size=PTC_SIZE, shuffle_indices=False
|
||||
// )
|
||||
// </spec>
|
||||
func PayloadCommittee(ctx context.Context, st state.ReadOnlyBeaconState, slot primitives.Slot) ([]primitives.ValidatorIndex, error) {
|
||||
// epoch = compute_epoch_at_slot(slot)
|
||||
// seed = hash(get_seed(state, epoch, DOMAIN_PTC_ATTESTER) + uint_to_bytes(slot))
|
||||
// indices = []
|
||||
// committees_per_slot = get_committee_count_per_slot(state, epoch)
|
||||
// for i in range(committees_per_slot):
|
||||
// committee = get_beacon_committee(state, slot, CommitteeIndex(i))
|
||||
// indices.extend(committee)
|
||||
// return compute_balance_weighted_selection(state, indices, seed, size=PTC_SIZE, shuffle_indices=False)
|
||||
func payloadCommittee(ctx context.Context, st state.ReadOnlyBeaconState, slot primitives.Slot) ([]primitives.ValidatorIndex, error) {
|
||||
epoch := slots.ToEpoch(slot)
|
||||
seed, err := ptcSeed(st, epoch, slot)
|
||||
if err != nil {
|
||||
@@ -166,35 +152,17 @@ func ptcSeed(st state.ReadOnlyBeaconState, epoch primitives.Epoch, slot primitiv
|
||||
}
|
||||
|
||||
// selectByBalance selects a balance-weighted subset of input candidates.
|
||||
// Spec v1.7.0-alpha.0 (pseudocode):
|
||||
// compute_balance_weighted_selection(state, indices, seed, size, shuffle_indices):
|
||||
// Note: shuffle_indices is false for PTC.
|
||||
//
|
||||
// <spec fn="compute_balance_weighted_selection" fork="gloas" hash="2c9f1c23">
|
||||
// def compute_balance_weighted_selection(
|
||||
// state: BeaconState,
|
||||
// indices: Sequence[ValidatorIndex],
|
||||
// seed: Bytes32,
|
||||
// size: uint64,
|
||||
// shuffle_indices: bool,
|
||||
// ) -> Sequence[ValidatorIndex]:
|
||||
// """
|
||||
// Return ``size`` indices sampled by effective balance, using ``indices``
|
||||
// as candidates. If ``shuffle_indices`` is ``True``, candidate indices
|
||||
// are themselves sampled from ``indices`` by shuffling it, otherwise
|
||||
// ``indices`` is traversed in order.
|
||||
// """
|
||||
// total = uint64(len(indices))
|
||||
// assert total > 0
|
||||
// selected: List[ValidatorIndex] = []
|
||||
// i = uint64(0)
|
||||
// while len(selected) < size:
|
||||
// next_index = i % total
|
||||
// if shuffle_indices:
|
||||
// next_index = compute_shuffled_index(next_index, total, seed)
|
||||
// candidate_index = indices[next_index]
|
||||
// if compute_balance_weighted_acceptance(state, candidate_index, seed, i):
|
||||
// selected.append(candidate_index)
|
||||
// i += 1
|
||||
// return selected
|
||||
// </spec>
|
||||
// total = len(indices); selected = []; i = 0
|
||||
// while len(selected) < size:
|
||||
// next = i % total
|
||||
// if shuffle_indices: next = compute_shuffled_index(next, total, seed)
|
||||
// if compute_balance_weighted_acceptance(state, indices[next], seed, i):
|
||||
// selected.append(indices[next])
|
||||
// i += 1
|
||||
func selectByBalanceFill(
|
||||
ctx context.Context,
|
||||
st state.ReadOnlyBeaconState,
|
||||
@@ -231,22 +199,15 @@ func selectByBalanceFill(
|
||||
}
|
||||
|
||||
// acceptByBalance determines if a validator is accepted based on its effective balance.
|
||||
// Spec v1.7.0-alpha.0 (pseudocode):
|
||||
// compute_balance_weighted_acceptance(state, index, seed, i):
|
||||
//
|
||||
// <spec fn="compute_balance_weighted_acceptance" fork="gloas" hash="9954dcd0">
|
||||
// def compute_balance_weighted_acceptance(
|
||||
// state: BeaconState, index: ValidatorIndex, seed: Bytes32, i: uint64
|
||||
// ) -> bool:
|
||||
// """
|
||||
// Return whether to accept the selection of the validator ``index``, with probability
|
||||
// proportional to its ``effective_balance``, and randomness given by ``seed`` and ``i``.
|
||||
// """
|
||||
// MAX_RANDOM_VALUE = 2**16 - 1
|
||||
// random_bytes = hash(seed + uint_to_bytes(i // 16))
|
||||
// offset = i % 16 * 2
|
||||
// random_value = bytes_to_uint64(random_bytes[offset : offset + 2])
|
||||
// effective_balance = state.validators[index].effective_balance
|
||||
// return effective_balance * MAX_RANDOM_VALUE >= MAX_EFFECTIVE_BALANCE_ELECTRA * random_value
|
||||
// </spec>
|
||||
// MAX_RANDOM_VALUE = 2**16 - 1
|
||||
// random_bytes = hash(seed + uint_to_bytes(i // 16))
|
||||
// offset = i % 16 * 2
|
||||
// random_value = bytes_to_uint64(random_bytes[offset:offset+2])
|
||||
// effective_balance = state.validators[index].effective_balance
|
||||
// return effective_balance * MAX_RANDOM_VALUE >= MAX_EFFECTIVE_BALANCE_ELECTRA * random_value
|
||||
func acceptByBalance(st state.ReadOnlyBeaconState, idx primitives.ValidatorIndex, seedBuf []byte, hashFunc func([]byte) [32]byte, maxBalance uint64, round uint64) (bool, error) {
|
||||
// Reuse the seed buffer by overwriting the last 8 bytes with the round counter.
|
||||
binary.LittleEndian.PutUint64(seedBuf[len(seedBuf)-8:], round/16)
|
||||
@@ -263,26 +224,16 @@ func acceptByBalance(st state.ReadOnlyBeaconState, idx primitives.ValidatorIndex
|
||||
}
|
||||
|
||||
// validIndexedPayloadAttestation verifies the signature of an indexed payload attestation.
|
||||
// Spec v1.7.0-alpha.0 (pseudocode):
|
||||
// is_valid_indexed_payload_attestation(state: BeaconState, indexed_payload_attestation: IndexedPayloadAttestation) -> bool:
|
||||
//
|
||||
// <spec fn="is_valid_indexed_payload_attestation" fork="gloas" hash="d76e0f89">
|
||||
// def is_valid_indexed_payload_attestation(
|
||||
// state: BeaconState, attestation: IndexedPayloadAttestation
|
||||
// ) -> bool:
|
||||
// """
|
||||
// Check if ``attestation`` is non-empty, has sorted indices, and has
|
||||
// a valid aggregate signature.
|
||||
// """
|
||||
// # Verify indices are non-empty and sorted
|
||||
// indices = attestation.attesting_indices
|
||||
// if len(indices) == 0 or not indices == sorted(indices):
|
||||
// return False
|
||||
//
|
||||
// # Verify aggregate signature
|
||||
// pubkeys = [state.validators[i].pubkey for i in indices]
|
||||
// domain = get_domain(state, DOMAIN_PTC_ATTESTER, compute_epoch_at_slot(attestation.data.slot))
|
||||
// signing_root = compute_signing_root(attestation.data, domain)
|
||||
// return bls.FastAggregateVerify(pubkeys, signing_root, attestation.signature)
|
||||
// </spec>
|
||||
// indices = indexed_payload_attestation.attesting_indices
|
||||
// return len(indices) > 0 and indices == sorted(indices) and
|
||||
// bls.FastAggregateVerify(
|
||||
// [state.validators[i].pubkey for i in indices],
|
||||
// compute_signing_root(indexed_payload_attestation.data, get_domain(state, DOMAIN_PTC_ATTESTER, compute_epoch_at_slot(attestation.data.slot)),
|
||||
// indexed_payload_attestation.signature,
|
||||
// )
|
||||
func validIndexedPayloadAttestation(st state.ReadOnlyBeaconState, att *consensus_types.IndexedPayloadAttestation) error {
|
||||
indices := att.AttestingIndices
|
||||
if len(indices) == 0 || !slices.IsSorted(indices) {
|
||||
|
||||
@@ -1,349 +0,0 @@
|
||||
package gloas
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
|
||||
state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native"
|
||||
"github.com/OffchainLabs/prysm/v7/config/params"
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
||||
"github.com/OffchainLabs/prysm/v7/crypto/bls"
|
||||
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
|
||||
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/require"
|
||||
"github.com/OffchainLabs/prysm/v7/time/slots"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type payloadFixture struct {
|
||||
state state.BeaconState
|
||||
signed interfaces.ROSignedExecutionPayloadEnvelope
|
||||
signedProto *ethpb.SignedExecutionPayloadEnvelope
|
||||
envelope *ethpb.ExecutionPayloadEnvelope
|
||||
payload *enginev1.ExecutionPayloadDeneb
|
||||
slot primitives.Slot
|
||||
}
|
||||
|
||||
func buildPayloadFixture(t *testing.T, mutate func(payload *enginev1.ExecutionPayloadDeneb, bid *ethpb.ExecutionPayloadBid, envelope *ethpb.ExecutionPayloadEnvelope)) payloadFixture {
|
||||
t.Helper()
|
||||
|
||||
cfg := params.BeaconConfig()
|
||||
slot := primitives.Slot(5)
|
||||
builderIdx := primitives.BuilderIndex(0)
|
||||
|
||||
sk, err := bls.RandKey()
|
||||
require.NoError(t, err)
|
||||
pk := sk.PublicKey().Marshal()
|
||||
|
||||
randao := bytes.Repeat([]byte{0xAA}, 32)
|
||||
parentHash := bytes.Repeat([]byte{0xBB}, 32)
|
||||
blockHash := bytes.Repeat([]byte{0xCC}, 32)
|
||||
|
||||
withdrawals := []*enginev1.Withdrawal{
|
||||
{Index: 0, ValidatorIndex: 1, Address: bytes.Repeat([]byte{0x01}, 20), Amount: 0},
|
||||
}
|
||||
|
||||
payload := &enginev1.ExecutionPayloadDeneb{
|
||||
ParentHash: parentHash,
|
||||
FeeRecipient: bytes.Repeat([]byte{0x01}, 20),
|
||||
StateRoot: bytes.Repeat([]byte{0x02}, 32),
|
||||
ReceiptsRoot: bytes.Repeat([]byte{0x03}, 32),
|
||||
LogsBloom: bytes.Repeat([]byte{0x04}, 256),
|
||||
PrevRandao: randao,
|
||||
BlockNumber: 1,
|
||||
GasLimit: 1,
|
||||
GasUsed: 0,
|
||||
Timestamp: 100,
|
||||
ExtraData: []byte{},
|
||||
BaseFeePerGas: bytes.Repeat([]byte{0x05}, 32),
|
||||
BlockHash: blockHash,
|
||||
Transactions: [][]byte{},
|
||||
Withdrawals: withdrawals,
|
||||
BlobGasUsed: 0,
|
||||
ExcessBlobGas: 0,
|
||||
}
|
||||
|
||||
bid := ðpb.ExecutionPayloadBid{
|
||||
ParentBlockHash: parentHash,
|
||||
ParentBlockRoot: bytes.Repeat([]byte{0xDD}, 32),
|
||||
BlockHash: blockHash,
|
||||
PrevRandao: randao,
|
||||
GasLimit: 1,
|
||||
BuilderIndex: builderIdx,
|
||||
Slot: slot,
|
||||
Value: 0,
|
||||
ExecutionPayment: 0,
|
||||
FeeRecipient: bytes.Repeat([]byte{0xEE}, 20),
|
||||
}
|
||||
|
||||
header := ðpb.BeaconBlockHeader{
|
||||
Slot: slot,
|
||||
ParentRoot: bytes.Repeat([]byte{0x11}, 32),
|
||||
StateRoot: bytes.Repeat([]byte{0x22}, 32),
|
||||
BodyRoot: bytes.Repeat([]byte{0x33}, 32),
|
||||
}
|
||||
headerRoot, err := header.HashTreeRoot()
|
||||
require.NoError(t, err)
|
||||
|
||||
envelope := ðpb.ExecutionPayloadEnvelope{
|
||||
Slot: slot,
|
||||
BuilderIndex: builderIdx,
|
||||
BeaconBlockRoot: headerRoot[:],
|
||||
Payload: payload,
|
||||
ExecutionRequests: &enginev1.ExecutionRequests{},
|
||||
}
|
||||
|
||||
if mutate != nil {
|
||||
mutate(payload, bid, envelope)
|
||||
}
|
||||
|
||||
genesisRoot := bytes.Repeat([]byte{0xAB}, 32)
|
||||
blockRoots := make([][]byte, cfg.SlotsPerHistoricalRoot)
|
||||
stateRoots := make([][]byte, cfg.SlotsPerHistoricalRoot)
|
||||
for i := range blockRoots {
|
||||
blockRoots[i] = bytes.Repeat([]byte{0x44}, 32)
|
||||
stateRoots[i] = bytes.Repeat([]byte{0x55}, 32)
|
||||
}
|
||||
randaoMixes := make([][]byte, cfg.EpochsPerHistoricalVector)
|
||||
for i := range randaoMixes {
|
||||
randaoMixes[i] = randao
|
||||
}
|
||||
|
||||
withdrawalCreds := make([]byte, 32)
|
||||
withdrawalCreds[0] = cfg.ETH1AddressWithdrawalPrefixByte
|
||||
|
||||
eth1Data := ðpb.Eth1Data{
|
||||
DepositRoot: bytes.Repeat([]byte{0x66}, 32),
|
||||
DepositCount: 0,
|
||||
BlockHash: bytes.Repeat([]byte{0x77}, 32),
|
||||
}
|
||||
|
||||
vals := []*ethpb.Validator{
|
||||
{
|
||||
PublicKey: pk,
|
||||
WithdrawalCredentials: withdrawalCreds,
|
||||
EffectiveBalance: cfg.MinActivationBalance + 1_000,
|
||||
},
|
||||
}
|
||||
balances := []uint64{cfg.MinActivationBalance + 1_000}
|
||||
|
||||
payments := make([]*ethpb.BuilderPendingPayment, cfg.SlotsPerEpoch*2)
|
||||
for i := range payments {
|
||||
payments[i] = ðpb.BuilderPendingPayment{
|
||||
Withdrawal: ðpb.BuilderPendingWithdrawal{
|
||||
FeeRecipient: make([]byte, 20),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
executionPayloadAvailability := make([]byte, cfg.SlotsPerHistoricalRoot/8)
|
||||
|
||||
builders := make([]*ethpb.Builder, builderIdx+1)
|
||||
builders[builderIdx] = ðpb.Builder{
|
||||
Pubkey: pk,
|
||||
Version: []byte{0},
|
||||
ExecutionAddress: bytes.Repeat([]byte{0x09}, 20),
|
||||
Balance: 0,
|
||||
DepositEpoch: 0,
|
||||
WithdrawableEpoch: 0,
|
||||
}
|
||||
|
||||
genesisTime := uint64(0)
|
||||
slotSeconds := cfg.SecondsPerSlot * uint64(slot)
|
||||
if payload.Timestamp > slotSeconds {
|
||||
genesisTime = payload.Timestamp - slotSeconds
|
||||
}
|
||||
|
||||
stProto := ðpb.BeaconStateGloas{
|
||||
Slot: slot,
|
||||
GenesisTime: genesisTime,
|
||||
GenesisValidatorsRoot: genesisRoot,
|
||||
Fork: ðpb.Fork{
|
||||
CurrentVersion: bytes.Repeat([]byte{0x01}, 4),
|
||||
PreviousVersion: bytes.Repeat([]byte{0x01}, 4),
|
||||
Epoch: 0,
|
||||
},
|
||||
LatestBlockHeader: header,
|
||||
BlockRoots: blockRoots,
|
||||
StateRoots: stateRoots,
|
||||
RandaoMixes: randaoMixes,
|
||||
Eth1Data: eth1Data,
|
||||
Validators: vals,
|
||||
Balances: balances,
|
||||
LatestBlockHash: payload.ParentHash,
|
||||
LatestExecutionPayloadBid: bid,
|
||||
BuilderPendingPayments: payments,
|
||||
ExecutionPayloadAvailability: executionPayloadAvailability,
|
||||
BuilderPendingWithdrawals: []*ethpb.BuilderPendingWithdrawal{},
|
||||
PayloadExpectedWithdrawals: payload.Withdrawals,
|
||||
Builders: builders,
|
||||
}
|
||||
|
||||
st, err := state_native.InitializeFromProtoGloas(stProto)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := st.Copy()
|
||||
ctx := context.Background()
|
||||
require.NoError(t, processExecutionRequests(ctx, expected, envelope.ExecutionRequests))
|
||||
require.NoError(t, expected.QueueBuilderPayment())
|
||||
require.NoError(t, expected.SetExecutionPayloadAvailability(slot, true))
|
||||
var blockHashArr [32]byte
|
||||
copy(blockHashArr[:], payload.BlockHash)
|
||||
require.NoError(t, expected.SetLatestBlockHash(blockHashArr))
|
||||
expectedRoot, err := expected.HashTreeRoot(ctx)
|
||||
require.NoError(t, err)
|
||||
envelope.StateRoot = expectedRoot[:]
|
||||
|
||||
epoch := slots.ToEpoch(slot)
|
||||
domain, err := signing.Domain(st.Fork(), epoch, cfg.DomainBeaconBuilder, st.GenesisValidatorsRoot())
|
||||
require.NoError(t, err)
|
||||
signingRoot, err := signing.ComputeSigningRoot(envelope, domain)
|
||||
require.NoError(t, err)
|
||||
signature := sk.Sign(signingRoot[:]).Marshal()
|
||||
|
||||
signedProto := ðpb.SignedExecutionPayloadEnvelope{
|
||||
Message: envelope,
|
||||
Signature: signature,
|
||||
}
|
||||
signed, err := blocks.WrappedROSignedExecutionPayloadEnvelope(signedProto)
|
||||
require.NoError(t, err)
|
||||
|
||||
return payloadFixture{
|
||||
state: st,
|
||||
signed: signed,
|
||||
signedProto: signedProto,
|
||||
envelope: envelope,
|
||||
payload: payload,
|
||||
slot: slot,
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessExecutionPayload_Success(t *testing.T) {
|
||||
fixture := buildPayloadFixture(t, nil)
|
||||
require.NoError(t, ProcessExecutionPayload(t.Context(), fixture.state, fixture.signed))
|
||||
|
||||
latestHash, err := fixture.state.LatestBlockHash()
|
||||
require.NoError(t, err)
|
||||
var expectedHash [32]byte
|
||||
copy(expectedHash[:], fixture.payload.BlockHash)
|
||||
require.Equal(t, expectedHash, latestHash)
|
||||
|
||||
slotsPerEpoch := params.BeaconConfig().SlotsPerEpoch
|
||||
paymentIndex := slotsPerEpoch + (fixture.slot % slotsPerEpoch)
|
||||
payments, err := fixture.state.BuilderPendingPayments()
|
||||
require.NoError(t, err)
|
||||
payment := payments[paymentIndex]
|
||||
require.NotNil(t, payment)
|
||||
require.Equal(t, primitives.Gwei(0), payment.Withdrawal.Amount)
|
||||
}
|
||||
|
||||
func TestProcessExecutionPayload_PrevRandaoMismatch(t *testing.T) {
|
||||
fixture := buildPayloadFixture(t, func(_ *enginev1.ExecutionPayloadDeneb, bid *ethpb.ExecutionPayloadBid, _ *ethpb.ExecutionPayloadEnvelope) {
|
||||
bid.PrevRandao = bytes.Repeat([]byte{0xFF}, 32)
|
||||
})
|
||||
|
||||
err := ProcessExecutionPayload(t.Context(), fixture.state, fixture.signed)
|
||||
require.ErrorContains(t, "prev randao", err)
|
||||
}
|
||||
|
||||
func TestQueueBuilderPayment_ZeroAmountClearsSlot(t *testing.T) {
|
||||
fixture := buildPayloadFixture(t, nil)
|
||||
|
||||
require.NoError(t, fixture.state.QueueBuilderPayment())
|
||||
|
||||
slotsPerEpoch := params.BeaconConfig().SlotsPerEpoch
|
||||
paymentIndex := slotsPerEpoch + (fixture.slot % slotsPerEpoch)
|
||||
payments, err := fixture.state.BuilderPendingPayments()
|
||||
require.NoError(t, err)
|
||||
payment := payments[paymentIndex]
|
||||
require.NotNil(t, payment)
|
||||
require.Equal(t, primitives.Gwei(0), payment.Withdrawal.Amount)
|
||||
}
|
||||
|
||||
func TestVerifyExecutionPayloadEnvelopeSignature(t *testing.T) {
|
||||
fixture := buildPayloadFixture(t, nil)
|
||||
|
||||
t.Run("self build", func(t *testing.T) {
|
||||
proposerSk, err := bls.RandKey()
|
||||
require.NoError(t, err)
|
||||
proposerPk := proposerSk.PublicKey().Marshal()
|
||||
|
||||
stPb, ok := fixture.state.ToProtoUnsafe().(*ethpb.BeaconStateGloas)
|
||||
require.Equal(t, true, ok)
|
||||
stPb = proto.Clone(stPb).(*ethpb.BeaconStateGloas)
|
||||
stPb.Validators[0].PublicKey = proposerPk
|
||||
st, err := state_native.InitializeFromProtoUnsafeGloas(stPb)
|
||||
require.NoError(t, err)
|
||||
|
||||
msg := proto.Clone(fixture.signedProto.Message).(*ethpb.ExecutionPayloadEnvelope)
|
||||
msg.BuilderIndex = params.BeaconConfig().BuilderIndexSelfBuild
|
||||
|
||||
epoch := slots.ToEpoch(msg.Slot)
|
||||
domain, err := signing.Domain(st.Fork(), epoch, params.BeaconConfig().DomainBeaconBuilder, st.GenesisValidatorsRoot())
|
||||
require.NoError(t, err)
|
||||
signingRoot, err := signing.ComputeSigningRoot(msg, domain)
|
||||
require.NoError(t, err)
|
||||
signature := proposerSk.Sign(signingRoot[:]).Marshal()
|
||||
|
||||
signedProto := ðpb.SignedExecutionPayloadEnvelope{
|
||||
Message: msg,
|
||||
Signature: signature,
|
||||
}
|
||||
signed, err := blocks.WrappedROSignedExecutionPayloadEnvelope(signedProto)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, verifyExecutionPayloadEnvelopeSignature(st, signed))
|
||||
})
|
||||
|
||||
t.Run("builder", func(t *testing.T) {
|
||||
signed, err := blocks.WrappedROSignedExecutionPayloadEnvelope(fixture.signedProto)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, verifyExecutionPayloadEnvelopeSignature(fixture.state, signed))
|
||||
})
|
||||
|
||||
t.Run("invalid signature", func(t *testing.T) {
|
||||
t.Run("self build", func(t *testing.T) {
|
||||
proposerSk, err := bls.RandKey()
|
||||
require.NoError(t, err)
|
||||
proposerPk := proposerSk.PublicKey().Marshal()
|
||||
|
||||
stPb, ok := fixture.state.ToProtoUnsafe().(*ethpb.BeaconStateGloas)
|
||||
require.Equal(t, true, ok)
|
||||
stPb = proto.Clone(stPb).(*ethpb.BeaconStateGloas)
|
||||
stPb.Validators[0].PublicKey = proposerPk
|
||||
st, err := state_native.InitializeFromProtoUnsafeGloas(stPb)
|
||||
require.NoError(t, err)
|
||||
|
||||
msg := proto.Clone(fixture.signedProto.Message).(*ethpb.ExecutionPayloadEnvelope)
|
||||
msg.BuilderIndex = params.BeaconConfig().BuilderIndexSelfBuild
|
||||
|
||||
signedProto := ðpb.SignedExecutionPayloadEnvelope{
|
||||
Message: msg,
|
||||
Signature: bytes.Repeat([]byte{0xFF}, 96),
|
||||
}
|
||||
badSigned, err := blocks.WrappedROSignedExecutionPayloadEnvelope(signedProto)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = verifyExecutionPayloadEnvelopeSignature(st, badSigned)
|
||||
require.ErrorContains(t, "invalid signature format", err)
|
||||
})
|
||||
|
||||
t.Run("builder", func(t *testing.T) {
|
||||
signedProto := ðpb.SignedExecutionPayloadEnvelope{
|
||||
Message: fixture.signedProto.Message,
|
||||
Signature: bytes.Repeat([]byte{0xFF}, 96),
|
||||
}
|
||||
badSigned, err := blocks.WrappedROSignedExecutionPayloadEnvelope(signedProto)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = verifyExecutionPayloadEnvelopeSignature(fixture.state, badSigned)
|
||||
require.ErrorContains(t, "invalid signature format", err)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -10,21 +10,17 @@ import (
|
||||
)
|
||||
|
||||
// ProcessBuilderPendingPayments processes the builder pending payments from the previous epoch.
|
||||
// Spec v1.7.0-alpha.0 (pseudocode):
|
||||
// def process_builder_pending_payments(state: BeaconState) -> None:
|
||||
//
|
||||
// <spec fn="process_builder_pending_payments" fork="gloas" hash="10da48dd">
|
||||
// def process_builder_pending_payments(state: BeaconState) -> None:
|
||||
// """
|
||||
// Processes the builder pending payments from the previous epoch.
|
||||
// """
|
||||
// quorum = get_builder_payment_quorum_threshold(state)
|
||||
// for payment in state.builder_pending_payments[:SLOTS_PER_EPOCH]:
|
||||
// if payment.weight >= quorum:
|
||||
// state.builder_pending_withdrawals.append(payment.withdrawal)
|
||||
// quorum = get_builder_payment_quorum_threshold(state)
|
||||
// for payment in state.builder_pending_payments[:SLOTS_PER_EPOCH]:
|
||||
// if payment.weight >= quorum:
|
||||
// state.builder_pending_withdrawals.append(payment.withdrawal)
|
||||
//
|
||||
// old_payments = state.builder_pending_payments[SLOTS_PER_EPOCH:]
|
||||
// new_payments = [BuilderPendingPayment() for _ in range(SLOTS_PER_EPOCH)]
|
||||
// state.builder_pending_payments = old_payments + new_payments
|
||||
// </spec>
|
||||
// old_payments = state.builder_pending_payments[SLOTS_PER_EPOCH:]
|
||||
// new_payments = [BuilderPendingPayment() for _ in range(SLOTS_PER_EPOCH)]
|
||||
// state.builder_pending_payments = old_payments + new_payments
|
||||
func ProcessBuilderPendingPayments(state state.BeaconState) error {
|
||||
quorum, err := builderQuorumThreshold(state)
|
||||
if err != nil {
|
||||
@@ -57,16 +53,12 @@ func ProcessBuilderPendingPayments(state state.BeaconState) error {
|
||||
}
|
||||
|
||||
// builderQuorumThreshold calculates the quorum threshold for builder payments.
|
||||
// Spec v1.7.0-alpha.0 (pseudocode):
|
||||
// def get_builder_payment_quorum_threshold(state: BeaconState) -> uint64:
|
||||
//
|
||||
// <spec fn="get_builder_payment_quorum_threshold" fork="gloas" hash="a64b7ffb">
|
||||
// def get_builder_payment_quorum_threshold(state: BeaconState) -> uint64:
|
||||
// """
|
||||
// Calculate the quorum threshold for builder payments.
|
||||
// """
|
||||
// per_slot_balance = get_total_active_balance(state) // SLOTS_PER_EPOCH
|
||||
// quorum = per_slot_balance * BUILDER_PAYMENT_THRESHOLD_NUMERATOR
|
||||
// return uint64(quorum // BUILDER_PAYMENT_THRESHOLD_DENOMINATOR)
|
||||
// </spec>
|
||||
// per_slot_balance = get_total_active_balance(state) // SLOTS_PER_EPOCH
|
||||
// quorum = per_slot_balance * BUILDER_PAYMENT_THRESHOLD_NUMERATOR
|
||||
// return uint64(quorum // BUILDER_PAYMENT_THRESHOLD_DENOMINATOR)
|
||||
func builderQuorumThreshold(state state.ReadOnlyBeaconState) (primitives.Gwei, error) {
|
||||
activeBalance, err := helpers.TotalActiveBalance(state)
|
||||
if err != nil {
|
||||
|
||||
@@ -11,20 +11,16 @@ import (
|
||||
)
|
||||
|
||||
// RemoveBuilderPendingPayment removes the pending builder payment for the proposal slot.
|
||||
// Spec v1.7.0 (pseudocode):
|
||||
//
|
||||
// <spec fn="process_proposer_slashing" fork="gloas" lines="22-32" hash="4da721ef">
|
||||
// # [New in Gloas:EIP7732]
|
||||
// # Remove the BuilderPendingPayment corresponding to
|
||||
// # this proposal if it is still in the 2-epoch window.
|
||||
// slot = header_1.slot
|
||||
// proposal_epoch = compute_epoch_at_slot(slot)
|
||||
// if proposal_epoch == get_current_epoch(state):
|
||||
// payment_index = SLOTS_PER_EPOCH + slot % SLOTS_PER_EPOCH
|
||||
// state.builder_pending_payments[payment_index] = BuilderPendingPayment()
|
||||
// payment_index = SLOTS_PER_EPOCH + slot % SLOTS_PER_EPOCH
|
||||
// state.builder_pending_payments[payment_index] = BuilderPendingPayment()
|
||||
// elif proposal_epoch == get_previous_epoch(state):
|
||||
// payment_index = slot % SLOTS_PER_EPOCH
|
||||
// state.builder_pending_payments[payment_index] = BuilderPendingPayment()
|
||||
// </spec>
|
||||
// payment_index = slot % SLOTS_PER_EPOCH
|
||||
// state.builder_pending_payments[payment_index] = BuilderPendingPayment()
|
||||
func RemoveBuilderPendingPayment(st state.BeaconState, header *eth.BeaconBlockHeader) error {
|
||||
proposalEpoch := slots.ToEpoch(header.Slot)
|
||||
currentEpoch := time.CurrentEpoch(st)
|
||||
|
||||
@@ -33,6 +33,7 @@ go_library(
|
||||
"@com_github_pkg_errors//:go_default_library",
|
||||
"@com_github_prometheus_client_golang//prometheus:go_default_library",
|
||||
"@com_github_prometheus_client_golang//prometheus/promauto:go_default_library",
|
||||
"@com_github_prysmaticlabs_go_bitfield//:go_default_library",
|
||||
"@org_golang_x_sync//errgroup:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package peerdas
|
||||
|
||||
import (
|
||||
stderrors "errors"
|
||||
"iter"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/blockchain/kzg"
|
||||
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
|
||||
"github.com/OffchainLabs/prysm/v7/config/params"
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
|
||||
"github.com/OffchainLabs/prysm/v7/container/trie"
|
||||
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@@ -16,6 +20,7 @@ var (
|
||||
ErrIndexTooLarge = errors.New("column index is larger than the specified columns count")
|
||||
ErrNoKzgCommitments = errors.New("no KZG commitments found")
|
||||
ErrMismatchLength = errors.New("mismatch in the length of the column, commitments or proofs")
|
||||
ErrEmptySegment = errors.New("empty segment in batch")
|
||||
ErrInvalidKZGProof = errors.New("invalid KZG proof")
|
||||
ErrBadRootLength = errors.New("bad root length")
|
||||
ErrInvalidInclusionProof = errors.New("invalid inclusion proof")
|
||||
@@ -57,80 +62,113 @@ func VerifyDataColumnSidecar(sidecar blocks.RODataColumn) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyDataColumnsSidecarKZGProofs verifies if the KZG proofs are correct.
|
||||
// CellProofBundleSegment is returned when a batch fails. The caller can call
|
||||
// the `.Verify` method to verify just this segment.
|
||||
type CellProofBundleSegment struct {
|
||||
indices []uint64
|
||||
commitments []kzg.Bytes48
|
||||
cells []kzg.Cell
|
||||
proofs []kzg.Bytes48
|
||||
}
|
||||
|
||||
// Verify verifies this segment without batching.
|
||||
func (s CellProofBundleSegment) Verify() error {
|
||||
if len(s.cells) == 0 {
|
||||
return ErrEmptySegment
|
||||
}
|
||||
verified, err := kzg.VerifyCellKZGProofBatch(s.commitments, s.indices, s.cells, s.proofs)
|
||||
if err != nil {
|
||||
return stderrors.Join(err, ErrInvalidKZGProof)
|
||||
}
|
||||
if !verified {
|
||||
return ErrInvalidKZGProof
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func VerifyDataColumnsCellsKZGProofs(sizeHint int, cellProofsIter iter.Seq[blocks.CellProofBundle]) error {
|
||||
// ignore the failed segment list since we are just passing in one segment.
|
||||
_, err := BatchVerifyDataColumnsCellsKZGProofs(sizeHint, []iter.Seq[blocks.CellProofBundle]{cellProofsIter})
|
||||
return err
|
||||
}
|
||||
|
||||
// BatchVerifyDataColumnsCellsKZGProofs verifies if the KZG proofs are correct.
|
||||
// Note: We are slightly deviating from the specification here:
|
||||
// The specification verifies the KZG proofs for each sidecar separately,
|
||||
// while we are verifying all the KZG proofs from multiple sidecars in a batch.
|
||||
// This is done to improve performance since the internal KZG library is way more
|
||||
// efficient when verifying in batch.
|
||||
// efficient when verifying in batch. If the batch fails, the failed segments
|
||||
// are returned to the caller so that they may try segment by segment without
|
||||
// batching. On success the failed segment list is empty.
|
||||
//
|
||||
// https://github.com/ethereum/consensus-specs/blob/master/specs/fulu/p2p-interface.md#verify_data_column_sidecar_kzg_proofs
|
||||
func VerifyDataColumnsSidecarKZGProofs(sidecars []blocks.RODataColumn) error {
|
||||
// Compute the total count.
|
||||
count := 0
|
||||
for _, sidecar := range sidecars {
|
||||
count += len(sidecar.Column)
|
||||
}
|
||||
func BatchVerifyDataColumnsCellsKZGProofs(sizeHint int, cellProofsIters []iter.Seq[blocks.CellProofBundle]) ( /* failed segment list */ []CellProofBundleSegment, error) {
|
||||
commitments := make([]kzg.Bytes48, 0, sizeHint)
|
||||
indices := make([]uint64, 0, sizeHint)
|
||||
cells := make([]kzg.Cell, 0, sizeHint)
|
||||
proofs := make([]kzg.Bytes48, 0, sizeHint)
|
||||
|
||||
commitments := make([]kzg.Bytes48, 0, count)
|
||||
indices := make([]uint64, 0, count)
|
||||
cells := make([]kzg.Cell, 0, count)
|
||||
proofs := make([]kzg.Bytes48, 0, count)
|
||||
|
||||
for _, sidecar := range sidecars {
|
||||
for i := range sidecar.Column {
|
||||
var anySegmentEmpty bool
|
||||
var segments []CellProofBundleSegment
|
||||
for _, cellProofsIter := range cellProofsIters {
|
||||
startIdx := len(cells)
|
||||
for bundle := range cellProofsIter {
|
||||
var (
|
||||
commitment kzg.Bytes48
|
||||
cell kzg.Cell
|
||||
proof kzg.Bytes48
|
||||
)
|
||||
|
||||
commitmentBytes := sidecar.KzgCommitments[i]
|
||||
cellBytes := sidecar.Column[i]
|
||||
proofBytes := sidecar.KzgProofs[i]
|
||||
|
||||
if len(commitmentBytes) != len(commitment) ||
|
||||
len(cellBytes) != len(cell) ||
|
||||
len(proofBytes) != len(proof) {
|
||||
return ErrMismatchLength
|
||||
if len(bundle.Commitment) != len(commitment) ||
|
||||
len(bundle.Cell) != len(cell) ||
|
||||
len(bundle.Proof) != len(proof) {
|
||||
return nil, ErrMismatchLength
|
||||
}
|
||||
|
||||
copy(commitment[:], commitmentBytes)
|
||||
copy(cell[:], cellBytes)
|
||||
copy(proof[:], proofBytes)
|
||||
copy(commitment[:], bundle.Commitment)
|
||||
copy(cell[:], bundle.Cell)
|
||||
copy(proof[:], bundle.Proof)
|
||||
|
||||
commitments = append(commitments, commitment)
|
||||
indices = append(indices, sidecar.Index)
|
||||
indices = append(indices, bundle.ColumnIndex)
|
||||
cells = append(cells, cell)
|
||||
proofs = append(proofs, proof)
|
||||
}
|
||||
if len(cells[startIdx:]) == 0 {
|
||||
anySegmentEmpty = true
|
||||
}
|
||||
segments = append(segments, CellProofBundleSegment{
|
||||
indices: indices[startIdx:],
|
||||
commitments: commitments[startIdx:],
|
||||
cells: cells[startIdx:],
|
||||
proofs: proofs[startIdx:],
|
||||
})
|
||||
}
|
||||
|
||||
if anySegmentEmpty {
|
||||
return segments, ErrEmptySegment
|
||||
}
|
||||
|
||||
// Batch verify that the cells match the corresponding commitments and proofs.
|
||||
verified, err := kzg.VerifyCellKZGProofBatch(commitments, indices, cells, proofs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "verify cell KZG proof batch")
|
||||
return segments, stderrors.Join(err, ErrInvalidKZGProof)
|
||||
}
|
||||
|
||||
if !verified {
|
||||
return ErrInvalidKZGProof
|
||||
return segments, ErrInvalidKZGProof
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// VerifyDataColumnSidecarInclusionProof verifies if the given KZG commitments included in the given beacon block.
|
||||
// https://github.com/ethereum/consensus-specs/blob/master/specs/fulu/p2p-interface.md#verify_data_column_sidecar_inclusion_proof
|
||||
func VerifyDataColumnSidecarInclusionProof(sidecar blocks.RODataColumn) error {
|
||||
if sidecar.SignedBlockHeader == nil || sidecar.SignedBlockHeader.Header == nil {
|
||||
return ErrNilBlockHeader
|
||||
}
|
||||
|
||||
root := sidecar.SignedBlockHeader.Header.BodyRoot
|
||||
if len(root) != fieldparams.RootLength {
|
||||
// verifyKzgCommitmentsInclusionProof is the shared implementation for inclusion proof verification.
|
||||
func verifyKzgCommitmentsInclusionProof(bodyRoot []byte, kzgCommitments [][]byte, inclusionProof [][]byte) error {
|
||||
if len(bodyRoot) != fieldparams.RootLength {
|
||||
return ErrBadRootLength
|
||||
}
|
||||
|
||||
leaves := blocks.LeavesFromCommitments(sidecar.KzgCommitments)
|
||||
leaves := blocks.LeavesFromCommitments(kzgCommitments)
|
||||
|
||||
sparse, err := trie.GenerateTrieFromItems(leaves, fieldparams.LogMaxBlobCommitments)
|
||||
if err != nil {
|
||||
@@ -142,7 +180,7 @@ func VerifyDataColumnSidecarInclusionProof(sidecar blocks.RODataColumn) error {
|
||||
return errors.Wrap(err, "hash tree root")
|
||||
}
|
||||
|
||||
verified := trie.VerifyMerkleProof(root, hashTreeRoot[:], kzgPosition, sidecar.KzgCommitmentsInclusionProof)
|
||||
verified := trie.VerifyMerkleProof(bodyRoot, hashTreeRoot[:], kzgPosition, inclusionProof)
|
||||
if !verified {
|
||||
return ErrInvalidInclusionProof
|
||||
}
|
||||
@@ -150,6 +188,31 @@ func VerifyDataColumnSidecarInclusionProof(sidecar blocks.RODataColumn) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyDataColumnSidecarInclusionProof verifies if the given KZG commitments included in the given beacon block.
|
||||
// https://github.com/ethereum/consensus-specs/blob/master/specs/fulu/p2p-interface.md#verify_data_column_sidecar_inclusion_proof
|
||||
func VerifyDataColumnSidecarInclusionProof(sidecar blocks.RODataColumn) error {
|
||||
if sidecar.SignedBlockHeader == nil || sidecar.SignedBlockHeader.Header == nil {
|
||||
return ErrNilBlockHeader
|
||||
}
|
||||
return verifyKzgCommitmentsInclusionProof(
|
||||
sidecar.SignedBlockHeader.Header.BodyRoot,
|
||||
sidecar.KzgCommitments,
|
||||
sidecar.KzgCommitmentsInclusionProof,
|
||||
)
|
||||
}
|
||||
|
||||
// VerifyPartialDataColumnHeaderInclusionProof verifies if the KZG commitments are included in the beacon block.
|
||||
func VerifyPartialDataColumnHeaderInclusionProof(header *ethpb.PartialDataColumnHeader) error {
|
||||
if header.SignedBlockHeader == nil || header.SignedBlockHeader.Header == nil {
|
||||
return ErrNilBlockHeader
|
||||
}
|
||||
return verifyKzgCommitmentsInclusionProof(
|
||||
header.SignedBlockHeader.Header.BodyRoot,
|
||||
header.KzgCommitments,
|
||||
header.KzgCommitmentsInclusionProof,
|
||||
)
|
||||
}
|
||||
|
||||
// ComputeSubnetForDataColumnSidecar computes the subnet for a data column sidecar.
|
||||
// https://github.com/ethereum/consensus-specs/blob/master/specs/fulu/p2p-interface.md#compute_subnet_for_data_column_sidecar
|
||||
func ComputeSubnetForDataColumnSidecar(columnIndex uint64) uint64 {
|
||||
|
||||
@@ -3,6 +3,7 @@ package peerdas_test
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"iter"
|
||||
"testing"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/blockchain/kzg"
|
||||
@@ -72,7 +73,7 @@ func TestVerifyDataColumnSidecarKZGProofs(t *testing.T) {
|
||||
sidecars := generateRandomSidecars(t, seed, blobCount)
|
||||
sidecars[0].Column[0] = sidecars[0].Column[0][:len(sidecars[0].Column[0])-1] // Remove one byte to create size mismatch
|
||||
|
||||
err := peerdas.VerifyDataColumnsSidecarKZGProofs(sidecars)
|
||||
err := peerdas.VerifyDataColumnsCellsKZGProofs(0, blocks.RODataColumnsToCellProofBundles(sidecars))
|
||||
require.ErrorIs(t, err, peerdas.ErrMismatchLength)
|
||||
})
|
||||
|
||||
@@ -80,14 +81,15 @@ func TestVerifyDataColumnSidecarKZGProofs(t *testing.T) {
|
||||
sidecars := generateRandomSidecars(t, seed, blobCount)
|
||||
sidecars[0].Column[0][0]++ // It is OK to overflow
|
||||
|
||||
err := peerdas.VerifyDataColumnsSidecarKZGProofs(sidecars)
|
||||
err := peerdas.VerifyDataColumnsCellsKZGProofs(0, blocks.RODataColumnsToCellProofBundles(sidecars))
|
||||
require.ErrorIs(t, err, peerdas.ErrInvalidKZGProof)
|
||||
})
|
||||
|
||||
t.Run("nominal", func(t *testing.T) {
|
||||
sidecars := generateRandomSidecars(t, seed, blobCount)
|
||||
err := peerdas.VerifyDataColumnsSidecarKZGProofs(sidecars)
|
||||
failedSegments, err := peerdas.BatchVerifyDataColumnsCellsKZGProofs(blobCount, []iter.Seq[blocks.CellProofBundle]{blocks.RODataColumnsToCellProofBundles(sidecars)})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, len(failedSegments))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -273,7 +275,7 @@ func BenchmarkVerifyDataColumnSidecarKZGProofs_SameCommitments_NoBatch(b *testin
|
||||
for _, sidecar := range sidecars {
|
||||
sidecars := []blocks.RODataColumn{sidecar}
|
||||
b.StartTimer()
|
||||
err := peerdas.VerifyDataColumnsSidecarKZGProofs(sidecars)
|
||||
err := peerdas.VerifyDataColumnsCellsKZGProofs(0, blocks.RODataColumnsToCellProofBundles(sidecars))
|
||||
b.StopTimer()
|
||||
require.NoError(b, err)
|
||||
}
|
||||
@@ -308,7 +310,7 @@ func BenchmarkVerifyDataColumnSidecarKZGProofs_DiffCommitments_Batch(b *testing.
|
||||
}
|
||||
|
||||
b.StartTimer()
|
||||
err := peerdas.VerifyDataColumnsSidecarKZGProofs(allSidecars)
|
||||
err := peerdas.VerifyDataColumnsCellsKZGProofs(0, blocks.RODataColumnsToCellProofBundles(allSidecars))
|
||||
b.StopTimer()
|
||||
require.NoError(b, err)
|
||||
}
|
||||
@@ -341,7 +343,7 @@ func BenchmarkVerifyDataColumnSidecarKZGProofs_DiffCommitments_Batch4(b *testing
|
||||
|
||||
for _, sidecars := range allSidecars {
|
||||
b.StartTimer()
|
||||
err := peerdas.VerifyDataColumnsSidecarKZGProofs(sidecars)
|
||||
err := peerdas.VerifyDataColumnsCellsKZGProofs(len(allSidecars), blocks.RODataColumnsToCellProofBundles(sidecars))
|
||||
b.StopTimer()
|
||||
require.NoError(b, err)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/OffchainLabs/go-bitfield"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/blockchain/kzg"
|
||||
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
|
||||
"github.com/OffchainLabs/prysm/v7/config/params"
|
||||
@@ -339,7 +340,8 @@ func ComputeCellsAndProofsFromFlat(blobs [][]byte, cellProofs [][]byte) ([][]kzg
|
||||
}
|
||||
|
||||
// ComputeCellsAndProofsFromStructured computes the cells and proofs from blobs and cell proofs.
|
||||
func ComputeCellsAndProofsFromStructured(blobsAndProofs []*pb.BlobAndProofV2) ([][]kzg.Cell, [][]kzg.Proof, error) {
|
||||
// commitmentCount is required to return the correct sized bitlist even if we see a nil slice of blobsAndProofs.
|
||||
func ComputeCellsAndProofsFromStructured(commitmentCount uint64, blobsAndProofs []*pb.BlobAndProofV2) (bitfield.Bitlist /* parts included */, [][]kzg.Cell, [][]kzg.Proof, error) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
cellsAndProofsFromStructuredComputationTime.Observe(float64(time.Since(start).Milliseconds()))
|
||||
@@ -347,14 +349,24 @@ func ComputeCellsAndProofsFromStructured(blobsAndProofs []*pb.BlobAndProofV2) ([
|
||||
|
||||
var wg errgroup.Group
|
||||
|
||||
cellsPerBlob := make([][]kzg.Cell, len(blobsAndProofs))
|
||||
proofsPerBlob := make([][]kzg.Proof, len(blobsAndProofs))
|
||||
var blobsPresent int
|
||||
for _, blobAndProof := range blobsAndProofs {
|
||||
if blobAndProof != nil {
|
||||
blobsPresent++
|
||||
}
|
||||
}
|
||||
cellsPerBlob := make([][]kzg.Cell, blobsPresent)
|
||||
proofsPerBlob := make([][]kzg.Proof, blobsPresent)
|
||||
included := bitfield.NewBitlist(commitmentCount)
|
||||
|
||||
var j int
|
||||
for i, blobAndProof := range blobsAndProofs {
|
||||
if blobAndProof == nil {
|
||||
return nil, nil, ErrNilBlobAndProof
|
||||
continue
|
||||
}
|
||||
included.SetBitAt(uint64(i), true)
|
||||
|
||||
compactIndex := j
|
||||
wg.Go(func() error {
|
||||
var kzgBlob kzg.Blob
|
||||
if copy(kzgBlob[:], blobAndProof.Blob) != len(kzgBlob) {
|
||||
@@ -381,17 +393,18 @@ func ComputeCellsAndProofsFromStructured(blobsAndProofs []*pb.BlobAndProofV2) ([
|
||||
kzgProofs = append(kzgProofs, kzgProof)
|
||||
}
|
||||
|
||||
cellsPerBlob[i] = cells
|
||||
proofsPerBlob[i] = kzgProofs
|
||||
cellsPerBlob[compactIndex] = cells
|
||||
proofsPerBlob[compactIndex] = kzgProofs
|
||||
return nil
|
||||
})
|
||||
j++
|
||||
}
|
||||
|
||||
if err := wg.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return cellsPerBlob, proofsPerBlob, nil
|
||||
return included, cellsPerBlob, proofsPerBlob, nil
|
||||
}
|
||||
|
||||
// ReconstructBlobs reconstructs blobs from data column sidecars without computing KZG proofs or creating sidecars.
|
||||
|
||||
@@ -479,8 +479,9 @@ func TestComputeCellsAndProofsFromFlat(t *testing.T) {
|
||||
|
||||
func TestComputeCellsAndProofsFromStructured(t *testing.T) {
|
||||
t.Run("nil blob and proof", func(t *testing.T) {
|
||||
_, _, err := peerdas.ComputeCellsAndProofsFromStructured([]*pb.BlobAndProofV2{nil})
|
||||
require.ErrorIs(t, err, peerdas.ErrNilBlobAndProof)
|
||||
included, _, _, err := peerdas.ComputeCellsAndProofsFromStructured(0, []*pb.BlobAndProofV2{nil})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(0), included.Count())
|
||||
})
|
||||
|
||||
t.Run("nominal", func(t *testing.T) {
|
||||
@@ -533,7 +534,8 @@ func TestComputeCellsAndProofsFromStructured(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test ComputeCellsAndProofs
|
||||
actualCellsPerBlob, actualProofsPerBlob, err := peerdas.ComputeCellsAndProofsFromStructured(blobsAndProofs)
|
||||
included, actualCellsPerBlob, actualProofsPerBlob, err := peerdas.ComputeCellsAndProofsFromStructured(uint64(len(blobsAndProofs)), blobsAndProofs)
|
||||
require.Equal(t, included.Count(), uint64(len(actualCellsPerBlob)))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, blobCount, len(actualCellsPerBlob))
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package peerdas
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/OffchainLabs/go-bitfield"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/blockchain/kzg"
|
||||
beaconState "github.com/OffchainLabs/prysm/v7/beacon-chain/state"
|
||||
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
|
||||
@@ -23,11 +24,13 @@ var (
|
||||
var (
|
||||
_ ConstructionPopulator = (*BlockReconstructionSource)(nil)
|
||||
_ ConstructionPopulator = (*SidecarReconstructionSource)(nil)
|
||||
_ ConstructionPopulator = (*PartialDataColumnHeaderReconstructionSource)(nil)
|
||||
)
|
||||
|
||||
const (
|
||||
BlockType = "BeaconBlock"
|
||||
SidecarType = "DataColumnSidecar"
|
||||
BlockType = "BeaconBlock"
|
||||
SidecarType = "DataColumnSidecar"
|
||||
PartialDataColumnHeaderType = "PartialDataColumnHeader"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -54,6 +57,10 @@ type (
|
||||
blocks.VerifiedRODataColumn
|
||||
}
|
||||
|
||||
PartialDataColumnHeaderReconstructionSource struct {
|
||||
*ethpb.PartialDataColumnHeader
|
||||
}
|
||||
|
||||
blockInfo struct {
|
||||
signedBlockHeader *ethpb.SignedBeaconBlockHeader
|
||||
kzgCommitments [][]byte
|
||||
@@ -71,6 +78,11 @@ func PopulateFromSidecar(sidecar blocks.VerifiedRODataColumn) *SidecarReconstruc
|
||||
return &SidecarReconstructionSource{VerifiedRODataColumn: sidecar}
|
||||
}
|
||||
|
||||
// PopulateFromPartialHeader creates a PartialDataColumnHeaderReconstructionSource from a partial header
|
||||
func PopulateFromPartialHeader(header *ethpb.PartialDataColumnHeader) *PartialDataColumnHeaderReconstructionSource {
|
||||
return &PartialDataColumnHeaderReconstructionSource{PartialDataColumnHeader: header}
|
||||
}
|
||||
|
||||
// ValidatorsCustodyRequirement returns the number of custody groups regarding the validator indices attached to the beacon node.
|
||||
// https://github.com/ethereum/consensus-specs/blob/master/specs/fulu/validator.md#validator-custody
|
||||
func ValidatorsCustodyRequirement(state beaconState.ReadOnlyBeaconState, validatorsIndex map[primitives.ValidatorIndex]bool) (uint64, error) {
|
||||
@@ -143,6 +155,40 @@ func DataColumnSidecars(cellsPerBlob [][]kzg.Cell, proofsPerBlob [][]kzg.Proof,
|
||||
return roSidecars, nil
|
||||
}
|
||||
|
||||
func PartialColumns(included bitfield.Bitlist, cellsPerBlob [][]kzg.Cell, proofsPerBlob [][]kzg.Proof, src ConstructionPopulator) ([]blocks.PartialDataColumn, error) {
|
||||
start := time.Now()
|
||||
const numberOfColumns = uint64(fieldparams.NumberOfColumns)
|
||||
cells, proofs, err := rotateRowsToCols(cellsPerBlob, proofsPerBlob, numberOfColumns)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "rotate cells and proofs")
|
||||
}
|
||||
info, err := src.extract()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "extract block info")
|
||||
}
|
||||
|
||||
dataColumns := make([]blocks.PartialDataColumn, 0, numberOfColumns)
|
||||
for idx := range numberOfColumns {
|
||||
dc, err := blocks.NewPartialDataColumn(info.signedBlockHeader, idx, info.kzgCommitments, info.kzgInclusionProof)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "new ro data column")
|
||||
}
|
||||
|
||||
for i := range len(info.kzgCommitments) {
|
||||
if !included.BitAt(uint64(i)) {
|
||||
continue
|
||||
}
|
||||
dc.ExtendFromVerfifiedCell(uint64(i), cells[idx][0], proofs[idx][0])
|
||||
cells[idx] = cells[idx][1:]
|
||||
proofs[idx] = proofs[idx][1:]
|
||||
}
|
||||
dataColumns = append(dataColumns, dc)
|
||||
}
|
||||
|
||||
dataColumnComputationTime.Observe(float64(time.Since(start).Milliseconds()))
|
||||
return dataColumns, nil
|
||||
}
|
||||
|
||||
// Slot returns the slot of the source
|
||||
func (s *BlockReconstructionSource) Slot() primitives.Slot {
|
||||
return s.Block().Slot()
|
||||
@@ -254,3 +300,43 @@ func (s *SidecarReconstructionSource) extract() (*blockInfo, error) {
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// Slot returns the slot from the partial data column header
|
||||
func (p *PartialDataColumnHeaderReconstructionSource) Slot() primitives.Slot {
|
||||
return p.SignedBlockHeader.Header.Slot
|
||||
}
|
||||
|
||||
// Root returns the block root computed from the header
|
||||
func (p *PartialDataColumnHeaderReconstructionSource) Root() [fieldparams.RootLength]byte {
|
||||
root, err := p.SignedBlockHeader.Header.HashTreeRoot()
|
||||
if err != nil {
|
||||
return [fieldparams.RootLength]byte{}
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
// ProposerIndex returns the proposer index from the header
|
||||
func (p *PartialDataColumnHeaderReconstructionSource) ProposerIndex() primitives.ValidatorIndex {
|
||||
return p.SignedBlockHeader.Header.ProposerIndex
|
||||
}
|
||||
|
||||
// Commitments returns the KZG commitments from the header
|
||||
func (p *PartialDataColumnHeaderReconstructionSource) Commitments() ([][]byte, error) {
|
||||
return p.KzgCommitments, nil
|
||||
}
|
||||
|
||||
// Type returns the type of the source
|
||||
func (p *PartialDataColumnHeaderReconstructionSource) Type() string {
|
||||
return PartialDataColumnHeaderType
|
||||
}
|
||||
|
||||
// extract extracts the block information from the partial header
|
||||
func (p *PartialDataColumnHeaderReconstructionSource) extract() (*blockInfo, error) {
|
||||
info := &blockInfo{
|
||||
signedBlockHeader: p.SignedBlockHeader,
|
||||
kzgCommitments: p.KzgCommitments,
|
||||
kzgInclusionProof: p.KzgCommitmentsInclusionProof,
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
@@ -267,4 +267,31 @@ func TestReconstructionSource(t *testing.T) {
|
||||
|
||||
require.Equal(t, peerdas.SidecarType, src.Type())
|
||||
})
|
||||
|
||||
t.Run("from partial header", func(t *testing.T) {
|
||||
referenceSidecar := sidecars[0]
|
||||
partialHeader := ðpb.PartialDataColumnHeader{
|
||||
SignedBlockHeader: referenceSidecar.SignedBlockHeader,
|
||||
KzgCommitments: referenceSidecar.KzgCommitments,
|
||||
KzgCommitmentsInclusionProof: referenceSidecar.KzgCommitmentsInclusionProof,
|
||||
}
|
||||
|
||||
src := peerdas.PopulateFromPartialHeader(partialHeader)
|
||||
require.Equal(t, referenceSidecar.SignedBlockHeader.Header.Slot, src.Slot())
|
||||
|
||||
// Compute expected root
|
||||
expectedRoot, err := referenceSidecar.SignedBlockHeader.Header.HashTreeRoot()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedRoot, src.Root())
|
||||
|
||||
require.Equal(t, referenceSidecar.SignedBlockHeader.Header.ProposerIndex, src.ProposerIndex())
|
||||
|
||||
commitments, err := src.Commitments()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(commitments))
|
||||
require.DeepEqual(t, commitment1, commitments[0])
|
||||
require.DeepEqual(t, commitment2, commitments[1])
|
||||
|
||||
require.Equal(t, peerdas.PartialDataColumnHeaderType, src.Type())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -143,11 +143,10 @@ func ProcessSlot(ctx context.Context, state state.BeaconState) (state.BeaconStat
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// <spec fn="process_slot" fork="gloas" lines="11-13" hash="62b28839">
|
||||
// Spec v1.6.1 (pseudocode):
|
||||
// # [New in Gloas:EIP7732]
|
||||
// # Unset the next payload availability
|
||||
// state.execution_payload_availability[(state.slot + 1) % SLOTS_PER_HISTORICAL_ROOT] = 0b0
|
||||
// </spec>
|
||||
if state.Version() >= version.Gloas {
|
||||
index := uint64((state.Slot() + 1) % params.BeaconConfig().SlotsPerHistoricalRoot)
|
||||
if err := state.UpdateExecutionPayloadAvailabilityAtIndex(index, 0x0); err != nil {
|
||||
|
||||
@@ -78,7 +78,7 @@ func newGloasState(t *testing.T, slot primitives.Slot, availability []byte) stat
|
||||
BlockHash: make([]byte, 32),
|
||||
PrevRandao: make([]byte, 32),
|
||||
FeeRecipient: make([]byte, 20),
|
||||
BlobKzgCommitments: [][]byte{make([]byte, 48)},
|
||||
BlobKzgCommitmentsRoot: make([]byte, 32),
|
||||
},
|
||||
Eth1Data: ðpb.Eth1Data{
|
||||
DepositRoot: make([]byte, 32),
|
||||
|
||||
@@ -66,10 +66,6 @@ type ReadOnlyDatabase interface {
|
||||
OriginCheckpointBlockRoot(ctx context.Context) ([32]byte, error)
|
||||
BackfillStatus(context.Context) (*dbval.BackfillStatus, error)
|
||||
|
||||
// Execution payload envelope operations (Gloas+).
|
||||
ExecutionPayloadEnvelope(ctx context.Context, blockRoot [32]byte) (*ethpb.SignedBlindedExecutionPayloadEnvelope, error)
|
||||
HasExecutionPayloadEnvelope(ctx context.Context, blockRoot [32]byte) bool
|
||||
|
||||
// P2P Metadata operations.
|
||||
MetadataSeqNum(ctx context.Context) (uint64, error)
|
||||
}
|
||||
@@ -119,10 +115,6 @@ type NoHeadAccessDatabase interface {
|
||||
SaveLightClientUpdate(ctx context.Context, period uint64, update interfaces.LightClientUpdate) error
|
||||
SaveLightClientBootstrap(ctx context.Context, blockRoot []byte, bootstrap interfaces.LightClientBootstrap) error
|
||||
|
||||
// Execution payload envelope operations (Gloas+).
|
||||
SaveExecutionPayloadEnvelope(ctx context.Context, envelope *ethpb.SignedExecutionPayloadEnvelope) error
|
||||
DeleteExecutionPayloadEnvelope(ctx context.Context, blockRoot [32]byte) error
|
||||
|
||||
CleanUpDirtyStates(ctx context.Context, slotsPerArchivedPoint primitives.Slot) error
|
||||
DeleteHistoricalDataBeforeSlot(ctx context.Context, slot primitives.Slot, batchSize int) (int, error)
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ go_library(
|
||||
"encoding.go",
|
||||
"error.go",
|
||||
"execution_chain.go",
|
||||
"execution_payload_envelope.go",
|
||||
"finalized_block_roots.go",
|
||||
"genesis.go",
|
||||
"key.go",
|
||||
@@ -97,7 +96,6 @@ go_test(
|
||||
"deposit_contract_test.go",
|
||||
"encoding_test.go",
|
||||
"execution_chain_test.go",
|
||||
"execution_payload_envelope_test.go",
|
||||
"finalized_block_roots_test.go",
|
||||
"genesis_test.go",
|
||||
"init_test.go",
|
||||
|
||||
@@ -2,7 +2,6 @@ package kv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
||||
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
|
||||
@@ -34,9 +33,6 @@ func (s *Store) LastArchivedRoot(ctx context.Context) [32]byte {
|
||||
if err := s.db.View(func(tx *bolt.Tx) error {
|
||||
bkt := tx.Bucket(stateSlotIndicesBucket)
|
||||
_, blockRoot = bkt.Cursor().Last()
|
||||
if len(blockRoot) > 0 {
|
||||
blockRoot = slices.Clone(blockRoot)
|
||||
}
|
||||
return nil
|
||||
}); err != nil { // This view never returns an error, but we'll handle anyway for sanity.
|
||||
panic(err) // lint:nopanic -- View never returns an error.
|
||||
@@ -55,9 +51,6 @@ func (s *Store) ArchivedPointRoot(ctx context.Context, slot primitives.Slot) [32
|
||||
if err := s.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket(stateSlotIndicesBucket)
|
||||
blockRoot = bucket.Get(bytesutil.SlotToBytesBigEndian(slot))
|
||||
if len(blockRoot) > 0 {
|
||||
blockRoot = slices.Clone(blockRoot)
|
||||
}
|
||||
return nil
|
||||
}); err != nil { // This view never returns an error, but we'll handle anyway for sanity.
|
||||
panic(err) // lint:nopanic -- View never returns an error.
|
||||
|
||||
@@ -517,10 +517,6 @@ func (s *Store) DeleteHistoricalDataBeforeSlot(ctx context.Context, cutoffSlot p
|
||||
return errors.Wrap(err, "could not delete validators")
|
||||
}
|
||||
|
||||
// TODO: execution payload envelopes (Gloas+) are keyed by execution payload
|
||||
// block hash, not beacon block root, so they cannot be pruned in this loop.
|
||||
// A separate pruning mechanism is needed (e.g. secondary index or cursor scan).
|
||||
|
||||
numSlotsDeleted++
|
||||
}
|
||||
|
||||
@@ -816,10 +812,7 @@ func (s *Store) FeeRecipientByValidatorID(ctx context.Context, id primitives.Val
|
||||
var addr []byte
|
||||
err := s.db.View(func(tx *bolt.Tx) error {
|
||||
bkt := tx.Bucket(feeRecipientBucket)
|
||||
stored := bkt.Get(bytesutil.Uint64ToBytesBigEndian(uint64(id)))
|
||||
if len(stored) > 0 {
|
||||
addr = slices.Clone(stored)
|
||||
}
|
||||
addr = bkt.Get(bytesutil.Uint64ToBytesBigEndian(uint64(id)))
|
||||
// IF the fee recipient is not found in the standard fee recipient bucket, then
|
||||
// check the registration bucket. The fee recipient may be there.
|
||||
// This is to resolve imcompatility until we fully migrate to the registration bucket.
|
||||
@@ -833,7 +826,7 @@ func (s *Store) FeeRecipientByValidatorID(ctx context.Context, id primitives.Val
|
||||
if err := decode(ctx, enc, reg); err != nil {
|
||||
return err
|
||||
}
|
||||
addr = slices.Clone(reg.FeeRecipient)
|
||||
addr = reg.FeeRecipient
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -1254,12 +1247,6 @@ func unmarshalBlock(_ context.Context, enc []byte) (interfaces.ReadOnlySignedBea
|
||||
if err := rawBlock.UnmarshalSSZ(enc[len(fuluBlindKey):]); err != nil {
|
||||
return nil, errors.Wrap(err, "could not unmarshal blinded Fulu block")
|
||||
}
|
||||
case hasGloasKey(enc):
|
||||
// post Gloas we save the full beacon block as EIP-7732 separates beacon block and payload
|
||||
rawBlock = ðpb.SignedBeaconBlockGloas{}
|
||||
if err := rawBlock.UnmarshalSSZ(enc[len(gloasKey):]); err != nil {
|
||||
return nil, errors.Wrap(err, "could not unmarshal Gloas block")
|
||||
}
|
||||
default:
|
||||
// Marshal block bytes to phase 0 beacon block.
|
||||
rawBlock = ðpb.SignedBeaconBlock{}
|
||||
@@ -1290,11 +1277,6 @@ func encodeBlock(blk interfaces.ReadOnlySignedBeaconBlock) ([]byte, error) {
|
||||
func keyForBlock(blk interfaces.ReadOnlySignedBeaconBlock) ([]byte, error) {
|
||||
v := blk.Version()
|
||||
|
||||
if v >= version.Gloas {
|
||||
// Gloas blocks are never blinded (no execution payload in block body).
|
||||
return gloasKey, nil
|
||||
}
|
||||
|
||||
if v >= version.Fulu {
|
||||
if blk.IsBlinded() {
|
||||
return fuluBlindKey, nil
|
||||
|
||||
@@ -151,17 +151,6 @@ var blockTests = []struct {
|
||||
}
|
||||
return blocks.NewSignedBeaconBlock(b)
|
||||
}},
|
||||
{
|
||||
name: "gloas",
|
||||
newBlock: func(slot primitives.Slot, root []byte) (interfaces.ReadOnlySignedBeaconBlock, error) {
|
||||
b := util.NewBeaconBlockGloas()
|
||||
b.Block.Slot = slot
|
||||
if root != nil {
|
||||
b.Block.ParentRoot = root
|
||||
}
|
||||
return blocks.NewSignedBeaconBlock(b)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestStore_SaveBlock_NoDuplicates(t *testing.T) {
|
||||
@@ -222,7 +211,7 @@ func TestStore_BlocksCRUD(t *testing.T) {
|
||||
retrievedBlock, err = db.Block(ctx, blockRoot)
|
||||
require.NoError(t, err)
|
||||
wanted := retrievedBlock
|
||||
if retrievedBlock.Version() >= version.Bellatrix && retrievedBlock.Version() < version.Gloas {
|
||||
if retrievedBlock.Version() >= version.Bellatrix {
|
||||
wanted, err = retrievedBlock.ToBlinded()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -654,7 +643,7 @@ func TestStore_BlocksCRUD_NoCache(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
wanted := blk
|
||||
if blk.Version() >= version.Bellatrix && blk.Version() < version.Gloas {
|
||||
if blk.Version() >= version.Bellatrix {
|
||||
wanted, err = blk.ToBlinded()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -1025,7 +1014,7 @@ func TestStore_SaveBlock_CanGetHighestAt(t *testing.T) {
|
||||
b, err := db.Block(ctx, root)
|
||||
require.NoError(t, err)
|
||||
wanted := block1
|
||||
if block1.Version() >= version.Bellatrix && block1.Version() < version.Gloas {
|
||||
if block1.Version() >= version.Bellatrix {
|
||||
wanted, err = wanted.ToBlinded()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -1043,7 +1032,7 @@ func TestStore_SaveBlock_CanGetHighestAt(t *testing.T) {
|
||||
b, err = db.Block(ctx, root)
|
||||
require.NoError(t, err)
|
||||
wanted2 := block2
|
||||
if block2.Version() >= version.Bellatrix && block2.Version() < version.Gloas {
|
||||
if block2.Version() >= version.Bellatrix {
|
||||
wanted2, err = block2.ToBlinded()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -1061,7 +1050,7 @@ func TestStore_SaveBlock_CanGetHighestAt(t *testing.T) {
|
||||
b, err = db.Block(ctx, root)
|
||||
require.NoError(t, err)
|
||||
wanted = block3
|
||||
if block3.Version() >= version.Bellatrix && block3.Version() < version.Gloas {
|
||||
if block3.Version() >= version.Bellatrix {
|
||||
wanted, err = wanted.ToBlinded()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -1097,7 +1086,7 @@ func TestStore_GenesisBlock_CanGetHighestAt(t *testing.T) {
|
||||
b, err := db.Block(ctx, root)
|
||||
require.NoError(t, err)
|
||||
wanted := block1
|
||||
if block1.Version() >= version.Bellatrix && block1.Version() < version.Gloas {
|
||||
if block1.Version() >= version.Bellatrix {
|
||||
wanted, err = block1.ToBlinded()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -1114,7 +1103,7 @@ func TestStore_GenesisBlock_CanGetHighestAt(t *testing.T) {
|
||||
b, err = db.Block(ctx, root)
|
||||
require.NoError(t, err)
|
||||
wanted = genesisBlock
|
||||
if genesisBlock.Version() >= version.Bellatrix && genesisBlock.Version() < version.Gloas {
|
||||
if genesisBlock.Version() >= version.Bellatrix {
|
||||
wanted, err = genesisBlock.ToBlinded()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -1131,7 +1120,7 @@ func TestStore_GenesisBlock_CanGetHighestAt(t *testing.T) {
|
||||
b, err = db.Block(ctx, root)
|
||||
require.NoError(t, err)
|
||||
wanted = genesisBlock
|
||||
if genesisBlock.Version() >= version.Bellatrix && genesisBlock.Version() < version.Gloas {
|
||||
if genesisBlock.Version() >= version.Bellatrix {
|
||||
wanted, err = genesisBlock.ToBlinded()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -1227,7 +1216,7 @@ func TestStore_BlocksBySlot_BlockRootsBySlot(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
wanted := b1
|
||||
if b1.Version() >= version.Bellatrix && b1.Version() < version.Gloas {
|
||||
if b1.Version() >= version.Bellatrix {
|
||||
wanted, err = b1.ToBlinded()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -1243,7 +1232,7 @@ func TestStore_BlocksBySlot_BlockRootsBySlot(t *testing.T) {
|
||||
t.Fatalf("Expected 2 blocks, received %d blocks", len(retrievedBlocks))
|
||||
}
|
||||
wanted = b2
|
||||
if b2.Version() >= version.Bellatrix && b2.Version() < version.Gloas {
|
||||
if b2.Version() >= version.Bellatrix {
|
||||
wanted, err = b2.ToBlinded()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -1253,7 +1242,7 @@ func TestStore_BlocksBySlot_BlockRootsBySlot(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, true, proto.Equal(wantedPb, retrieved0Pb), "Wanted: %v, received: %v", retrievedBlocks[0], wanted)
|
||||
wanted = b3
|
||||
if b3.Version() >= version.Bellatrix && b3.Version() < version.Gloas {
|
||||
if b3.Version() >= version.Bellatrix {
|
||||
wanted, err = b3.ToBlinded()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package kv
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
@@ -18,10 +17,7 @@ func (s *Store) DepositContractAddress(ctx context.Context) ([]byte, error) {
|
||||
var addr []byte
|
||||
if err := s.db.View(func(tx *bolt.Tx) error {
|
||||
chainInfo := tx.Bucket(chainMetadataBucket)
|
||||
stored := chainInfo.Get(depositContractAddressKey)
|
||||
if len(stored) > 0 {
|
||||
addr = slices.Clone(stored)
|
||||
}
|
||||
addr = chainInfo.Get(depositContractAddressKey)
|
||||
return nil
|
||||
}); err != nil { // This view never returns an error, but we'll handle anyway for sanity.
|
||||
panic(err) // lint:nopanic -- View never returns an error.
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
package kv
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
|
||||
"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
|
||||
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
"github.com/golang/snappy"
|
||||
"github.com/pkg/errors"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
// SaveExecutionPayloadEnvelope blinds and saves a signed execution payload envelope keyed by
|
||||
// beacon block root. The envelope is stored in blinded form: the full execution payload is replaced
|
||||
// with its block hash. The full payload can later be retrieved from the EL via
|
||||
// engine_getPayloadBodiesByHash.
|
||||
func (s *Store) SaveExecutionPayloadEnvelope(ctx context.Context, env *ethpb.SignedExecutionPayloadEnvelope) error {
|
||||
_, span := trace.StartSpan(ctx, "BeaconDB.SaveExecutionPayloadEnvelope")
|
||||
defer span.End()
|
||||
|
||||
if env == nil || env.Message == nil || env.Message.Payload == nil {
|
||||
return errors.New("cannot save nil execution payload envelope")
|
||||
}
|
||||
|
||||
blockRoot := bytesutil.ToBytes32(env.Message.BeaconBlockRoot)
|
||||
blinded := blindEnvelope(env)
|
||||
|
||||
enc, err := encodeBlindedEnvelope(blinded)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.db.Update(func(tx *bolt.Tx) error {
|
||||
bkt := tx.Bucket(executionPayloadEnvelopesBucket)
|
||||
return bkt.Put(blockRoot[:], enc)
|
||||
})
|
||||
}
|
||||
|
||||
// ExecutionPayloadEnvelope retrieves the blinded signed execution payload envelope by beacon block root.
|
||||
func (s *Store) ExecutionPayloadEnvelope(ctx context.Context, blockRoot [32]byte) (*ethpb.SignedBlindedExecutionPayloadEnvelope, error) {
|
||||
_, span := trace.StartSpan(ctx, "BeaconDB.ExecutionPayloadEnvelope")
|
||||
defer span.End()
|
||||
|
||||
var enc []byte
|
||||
if err := s.db.View(func(tx *bolt.Tx) error {
|
||||
bkt := tx.Bucket(executionPayloadEnvelopesBucket)
|
||||
enc = bkt.Get(blockRoot[:])
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if enc == nil {
|
||||
return nil, errors.Wrap(ErrNotFound, "execution payload envelope not found")
|
||||
}
|
||||
return decodeBlindedEnvelope(enc)
|
||||
}
|
||||
|
||||
// HasExecutionPayloadEnvelope checks whether an execution payload envelope exists for the given beacon block root.
|
||||
func (s *Store) HasExecutionPayloadEnvelope(ctx context.Context, blockRoot [32]byte) bool {
|
||||
_, span := trace.StartSpan(ctx, "BeaconDB.HasExecutionPayloadEnvelope")
|
||||
defer span.End()
|
||||
|
||||
var exists bool
|
||||
if err := s.db.View(func(tx *bolt.Tx) error {
|
||||
bkt := tx.Bucket(executionPayloadEnvelopesBucket)
|
||||
exists = bkt.Get(blockRoot[:]) != nil
|
||||
return nil
|
||||
}); err != nil {
|
||||
return false
|
||||
}
|
||||
return exists
|
||||
}
|
||||
|
||||
// DeleteExecutionPayloadEnvelope removes a signed execution payload envelope by beacon block root.
|
||||
func (s *Store) DeleteExecutionPayloadEnvelope(ctx context.Context, blockRoot [32]byte) error {
|
||||
_, span := trace.StartSpan(ctx, "BeaconDB.DeleteExecutionPayloadEnvelope")
|
||||
defer span.End()
|
||||
|
||||
return s.db.Update(func(tx *bolt.Tx) error {
|
||||
bkt := tx.Bucket(executionPayloadEnvelopesBucket)
|
||||
return bkt.Delete(blockRoot[:])
|
||||
})
|
||||
}
|
||||
|
||||
// blindEnvelope converts a full signed envelope to its blinded form by replacing
|
||||
// the execution payload with its block hash. This avoids computing the expensive
|
||||
// payload hash tree root on the critical path.
|
||||
func blindEnvelope(env *ethpb.SignedExecutionPayloadEnvelope) *ethpb.SignedBlindedExecutionPayloadEnvelope {
|
||||
return ðpb.SignedBlindedExecutionPayloadEnvelope{
|
||||
Message: ðpb.BlindedExecutionPayloadEnvelope{
|
||||
BlockHash: env.Message.Payload.BlockHash,
|
||||
ExecutionRequests: env.Message.ExecutionRequests,
|
||||
BuilderIndex: env.Message.BuilderIndex,
|
||||
BeaconBlockRoot: env.Message.BeaconBlockRoot,
|
||||
Slot: env.Message.Slot,
|
||||
StateRoot: env.Message.StateRoot,
|
||||
},
|
||||
Signature: env.Signature,
|
||||
}
|
||||
}
|
||||
|
||||
// encodeBlindedEnvelope SSZ-encodes and snappy-compresses a blinded envelope for storage.
|
||||
func encodeBlindedEnvelope(env *ethpb.SignedBlindedExecutionPayloadEnvelope) ([]byte, error) {
|
||||
sszBytes, err := env.MarshalSSZ()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not marshal blinded envelope")
|
||||
}
|
||||
return snappy.Encode(nil, sszBytes), nil
|
||||
}
|
||||
|
||||
// decodeBlindedEnvelope snappy-decompresses and SSZ-decodes a blinded envelope from storage.
|
||||
func decodeBlindedEnvelope(enc []byte) (*ethpb.SignedBlindedExecutionPayloadEnvelope, error) {
|
||||
dec, err := snappy.Decode(nil, enc)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not snappy decode envelope")
|
||||
}
|
||||
blinded := ðpb.SignedBlindedExecutionPayloadEnvelope{}
|
||||
if err := blinded.UnmarshalSSZ(dec); err != nil {
|
||||
return nil, errors.Wrap(err, "could not unmarshal blinded envelope")
|
||||
}
|
||||
return blinded, nil
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
package kv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
||||
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
|
||||
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
|
||||
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/assert"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/require"
|
||||
)
|
||||
|
||||
func testEnvelope(t *testing.T) *ethpb.SignedExecutionPayloadEnvelope {
|
||||
t.Helper()
|
||||
return ðpb.SignedExecutionPayloadEnvelope{
|
||||
Message: ðpb.ExecutionPayloadEnvelope{
|
||||
Payload: &enginev1.ExecutionPayloadDeneb{
|
||||
ParentHash: bytesutil.PadTo([]byte("parent"), 32),
|
||||
FeeRecipient: bytesutil.PadTo([]byte("fee"), 20),
|
||||
StateRoot: bytesutil.PadTo([]byte("stateroot"), 32),
|
||||
ReceiptsRoot: bytesutil.PadTo([]byte("receipts"), 32),
|
||||
LogsBloom: bytesutil.PadTo([]byte{}, 256),
|
||||
PrevRandao: bytesutil.PadTo([]byte("randao"), 32),
|
||||
BlockNumber: 100,
|
||||
GasLimit: 30000000,
|
||||
GasUsed: 21000,
|
||||
Timestamp: 1000,
|
||||
ExtraData: []byte("extra"),
|
||||
BaseFeePerGas: bytesutil.PadTo([]byte{1}, 32),
|
||||
BlockHash: bytesutil.PadTo([]byte("blockhash"), 32),
|
||||
Transactions: [][]byte{[]byte("tx1"), []byte("tx2")},
|
||||
Withdrawals: []*enginev1.Withdrawal{{Index: 1, ValidatorIndex: 2, Address: bytesutil.PadTo([]byte("addr"), 20), Amount: 100}},
|
||||
BlobGasUsed: 131072,
|
||||
ExcessBlobGas: 0,
|
||||
},
|
||||
ExecutionRequests: &enginev1.ExecutionRequests{},
|
||||
BuilderIndex: primitives.BuilderIndex(42),
|
||||
BeaconBlockRoot: bytesutil.PadTo([]byte("beaconroot"), 32),
|
||||
Slot: primitives.Slot(99),
|
||||
StateRoot: bytesutil.PadTo([]byte("envelopestateroot"), 32),
|
||||
},
|
||||
Signature: bytesutil.PadTo([]byte("sig"), 96),
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_SaveAndRetrieveExecutionPayloadEnvelope(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
ctx := context.Background()
|
||||
env := testEnvelope(t)
|
||||
|
||||
// Keyed by beacon block root.
|
||||
blockRoot := bytesutil.ToBytes32(env.Message.BeaconBlockRoot)
|
||||
|
||||
// Initially should not exist.
|
||||
assert.Equal(t, false, db.HasExecutionPayloadEnvelope(ctx, blockRoot))
|
||||
|
||||
// Save (always blinds internally).
|
||||
require.NoError(t, db.SaveExecutionPayloadEnvelope(ctx, env))
|
||||
|
||||
// Should exist now.
|
||||
assert.Equal(t, true, db.HasExecutionPayloadEnvelope(ctx, blockRoot))
|
||||
|
||||
// Load and verify it's blinded.
|
||||
loaded, err := db.ExecutionPayloadEnvelope(ctx, blockRoot)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify metadata is preserved.
|
||||
assert.Equal(t, env.Message.Slot, loaded.Message.Slot)
|
||||
assert.Equal(t, env.Message.BuilderIndex, loaded.Message.BuilderIndex)
|
||||
assert.DeepEqual(t, env.Message.BeaconBlockRoot, loaded.Message.BeaconBlockRoot)
|
||||
assert.DeepEqual(t, env.Message.StateRoot, loaded.Message.StateRoot)
|
||||
assert.DeepEqual(t, env.Signature, loaded.Signature)
|
||||
|
||||
// BlockHash should be the payload's block hash (not a hash tree root).
|
||||
assert.DeepEqual(t, env.Message.Payload.BlockHash, loaded.Message.BlockHash)
|
||||
}
|
||||
|
||||
func TestStore_DeleteExecutionPayloadEnvelope(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
ctx := context.Background()
|
||||
env := testEnvelope(t)
|
||||
blockRoot := bytesutil.ToBytes32(env.Message.BeaconBlockRoot)
|
||||
|
||||
require.NoError(t, db.SaveExecutionPayloadEnvelope(ctx, env))
|
||||
assert.Equal(t, true, db.HasExecutionPayloadEnvelope(ctx, blockRoot))
|
||||
|
||||
require.NoError(t, db.DeleteExecutionPayloadEnvelope(ctx, blockRoot))
|
||||
assert.Equal(t, false, db.HasExecutionPayloadEnvelope(ctx, blockRoot))
|
||||
}
|
||||
|
||||
func TestStore_ExecutionPayloadEnvelope_NotFound(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
ctx := context.Background()
|
||||
nonExistent := bytesutil.ToBytes32([]byte("nonexistent"))
|
||||
|
||||
_, err := db.ExecutionPayloadEnvelope(ctx, nonExistent)
|
||||
require.ErrorContains(t, "not found", err)
|
||||
}
|
||||
|
||||
func TestStore_SaveExecutionPayloadEnvelope_NilRejected(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
err := db.SaveExecutionPayloadEnvelope(ctx, nil)
|
||||
require.ErrorContains(t, "nil", err)
|
||||
}
|
||||
|
||||
func TestBlindEnvelope_PreservesBlockHash(t *testing.T) {
|
||||
env := testEnvelope(t)
|
||||
|
||||
blinded := blindEnvelope(env)
|
||||
|
||||
// Should contain the block hash from the payload, not a hash tree root.
|
||||
assert.DeepEqual(t, env.Message.Payload.BlockHash, blinded.Message.BlockHash)
|
||||
|
||||
// Metadata should be preserved.
|
||||
assert.Equal(t, env.Message.BuilderIndex, blinded.Message.BuilderIndex)
|
||||
assert.Equal(t, env.Message.Slot, blinded.Message.Slot)
|
||||
assert.DeepEqual(t, env.Message.BeaconBlockRoot, blinded.Message.BeaconBlockRoot)
|
||||
assert.DeepEqual(t, env.Message.StateRoot, blinded.Message.StateRoot)
|
||||
assert.DeepEqual(t, env.Signature, blinded.Signature)
|
||||
}
|
||||
@@ -87,10 +87,3 @@ func hasFuluBlindKey(enc []byte) bool {
|
||||
}
|
||||
return bytes.Equal(enc[:len(fuluBlindKey)], fuluBlindKey)
|
||||
}
|
||||
|
||||
func hasGloasKey(enc []byte) bool {
|
||||
if len(gloasKey) >= len(enc) {
|
||||
return false
|
||||
}
|
||||
return bytes.Equal(enc[:len(gloasKey)], gloasKey)
|
||||
}
|
||||
|
||||
@@ -126,7 +126,6 @@ var Buckets = [][]byte{
|
||||
feeRecipientBucket,
|
||||
registrationBucket,
|
||||
custodyBucket,
|
||||
executionPayloadEnvelopesBucket,
|
||||
}
|
||||
|
||||
// KVStoreOption is a functional option that modifies a kv.Store.
|
||||
|
||||
@@ -199,7 +199,7 @@ func performValidatorStateMigration(ctx context.Context, bar *progressbar.Progre
|
||||
func stateBucketKeys(stateBucket *bolt.Bucket) ([][]byte, error) {
|
||||
var keys [][]byte
|
||||
if err := stateBucket.ForEach(func(pubKey, v []byte) error {
|
||||
keys = append(keys, bytes.Clone(pubKey))
|
||||
keys = append(keys, pubKey)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -7,17 +7,16 @@ package kv
|
||||
// it easy to scan for keys that have a certain shard number as a prefix and return those
|
||||
// corresponding attestations.
|
||||
var (
|
||||
blocksBucket = []byte("blocks")
|
||||
stateBucket = []byte("state")
|
||||
stateSummaryBucket = []byte("state-summary")
|
||||
chainMetadataBucket = []byte("chain-metadata")
|
||||
checkpointBucket = []byte("check-point")
|
||||
powchainBucket = []byte("powchain")
|
||||
stateValidatorsBucket = []byte("state-validators")
|
||||
feeRecipientBucket = []byte("fee-recipient")
|
||||
registrationBucket = []byte("registration")
|
||||
stateDiffBucket = []byte("state-diff")
|
||||
executionPayloadEnvelopesBucket = []byte("execution-payload-envelopes")
|
||||
blocksBucket = []byte("blocks")
|
||||
stateBucket = []byte("state")
|
||||
stateSummaryBucket = []byte("state-summary")
|
||||
chainMetadataBucket = []byte("chain-metadata")
|
||||
checkpointBucket = []byte("check-point")
|
||||
powchainBucket = []byte("powchain")
|
||||
stateValidatorsBucket = []byte("state-validators")
|
||||
feeRecipientBucket = []byte("fee-recipient")
|
||||
registrationBucket = []byte("registration")
|
||||
stateDiffBucket = []byte("state-diff")
|
||||
|
||||
// Light Client Updates Bucket
|
||||
lightClientUpdatesBucket = []byte("light-client-updates")
|
||||
@@ -61,8 +60,6 @@ var (
|
||||
electraBlindKey = []byte("blind-electra")
|
||||
fuluKey = []byte("fulu")
|
||||
fuluBlindKey = []byte("blind-fulu")
|
||||
gloasKey = []byte("gloas")
|
||||
// No gloasBlindKey needed - Gloas blocks are never blinded (no execution payload in block body).
|
||||
|
||||
// block root included in the beacon state used by weak subjectivity initial sync
|
||||
originCheckpointBlockRootKey = []byte("origin-checkpoint-block-root")
|
||||
|
||||
@@ -2,7 +2,6 @@ package kv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
|
||||
"github.com/OffchainLabs/prysm/v7/cmd/beacon-chain/flags"
|
||||
@@ -188,23 +187,20 @@ func (s *Store) getDiff(lvl int, slot uint64) (hdiff.HdiffBytes, error) {
|
||||
return bolt.ErrBucketNotFound
|
||||
}
|
||||
buf := append(key, stateSuffix...)
|
||||
rawStateDiff := bucket.Get(buf)
|
||||
if len(rawStateDiff) == 0 {
|
||||
stateDiff = bucket.Get(buf)
|
||||
if stateDiff == nil {
|
||||
return errors.New("state diff not found")
|
||||
}
|
||||
stateDiff = slices.Clone(rawStateDiff)
|
||||
buf = append(key, validatorSuffix...)
|
||||
rawValidatorDiff := bucket.Get(buf)
|
||||
if len(rawValidatorDiff) == 0 {
|
||||
validatorDiff = bucket.Get(buf)
|
||||
if validatorDiff == nil {
|
||||
return errors.New("validator diff not found")
|
||||
}
|
||||
validatorDiff = slices.Clone(rawValidatorDiff)
|
||||
buf = append(key, balancesSuffix...)
|
||||
rawBalancesDiff := bucket.Get(buf)
|
||||
if len(rawBalancesDiff) == 0 {
|
||||
balancesDiff = bucket.Get(buf)
|
||||
if balancesDiff == nil {
|
||||
return errors.New("balances diff not found")
|
||||
}
|
||||
balancesDiff = slices.Clone(rawBalancesDiff)
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -228,11 +224,10 @@ func (s *Store) getFullSnapshot(slot uint64) (state.BeaconState, error) {
|
||||
if bucket == nil {
|
||||
return bolt.ErrBucketNotFound
|
||||
}
|
||||
rawEnc := bucket.Get(key)
|
||||
if rawEnc == nil {
|
||||
enc = bucket.Get(key)
|
||||
if enc == nil {
|
||||
return errors.New("state not found")
|
||||
}
|
||||
enc = slices.Clone(rawEnc)
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package kv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
|
||||
"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
|
||||
@@ -48,11 +47,7 @@ func (s *Store) StateSummary(ctx context.Context, blockRoot [32]byte) (*ethpb.St
|
||||
}
|
||||
var enc []byte
|
||||
if err := s.db.View(func(tx *bolt.Tx) error {
|
||||
rawEnc := tx.Bucket(stateSummaryBucket).Get(blockRoot[:])
|
||||
if len(rawEnc) == 0 {
|
||||
return nil
|
||||
}
|
||||
enc = slices.Clone(rawEnc)
|
||||
enc = tx.Bucket(stateSummaryBucket).Get(blockRoot[:])
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -8,7 +8,6 @@ go_library(
|
||||
"deposit.go",
|
||||
"engine_client.go",
|
||||
"errors.go",
|
||||
"graffiti_info.go",
|
||||
"log.go",
|
||||
"log_processing.go",
|
||||
"metrics.go",
|
||||
@@ -74,6 +73,7 @@ go_library(
|
||||
"@com_github_pkg_errors//:go_default_library",
|
||||
"@com_github_prometheus_client_golang//prometheus:go_default_library",
|
||||
"@com_github_prometheus_client_golang//prometheus/promauto:go_default_library",
|
||||
"@com_github_prysmaticlabs_go_bitfield//:go_default_library",
|
||||
"@com_github_sirupsen_logrus//:go_default_library",
|
||||
"@io_k8s_client_go//tools/cache:go_default_library",
|
||||
"@org_golang_google_protobuf//proto:go_default_library",
|
||||
@@ -90,7 +90,6 @@ go_test(
|
||||
"engine_client_fuzz_test.go",
|
||||
"engine_client_test.go",
|
||||
"execution_chain_test.go",
|
||||
"graffiti_info_test.go",
|
||||
"init_test.go",
|
||||
"log_processing_test.go",
|
||||
"mock_test.go",
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/OffchainLabs/go-bitfield"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/blockchain/kzg"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/peerdas"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/execution/types"
|
||||
@@ -58,20 +59,11 @@ var (
|
||||
fuluEngineEndpoints = []string{
|
||||
GetPayloadMethodV5,
|
||||
GetBlobsV2,
|
||||
GetBlobsV3,
|
||||
}
|
||||
)
|
||||
|
||||
// ClientVersionV1 represents the response from engine_getClientVersionV1.
|
||||
type ClientVersionV1 struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Commit string `json:"commit"`
|
||||
}
|
||||
|
||||
const (
|
||||
// GetClientVersionMethod is the engine_getClientVersionV1 method for JSON-RPC.
|
||||
GetClientVersionMethod = "engine_getClientVersionV1"
|
||||
// NewPayloadMethod v1 request string for JSON-RPC.
|
||||
NewPayloadMethod = "engine_newPayloadV1"
|
||||
// NewPayloadMethodV2 v2 request string for JSON-RPC.
|
||||
@@ -109,6 +101,8 @@ const (
|
||||
GetBlobsV1 = "engine_getBlobsV1"
|
||||
// GetBlobsV2 request string for JSON-RPC.
|
||||
GetBlobsV2 = "engine_getBlobsV2"
|
||||
// GetBlobsV3 request string for JSON-RPC.
|
||||
GetBlobsV3 = "engine_getBlobsV3"
|
||||
// Defines the seconds before timing out engine endpoints with non-block execution semantics.
|
||||
defaultEngineTimeout = time.Second
|
||||
)
|
||||
@@ -132,7 +126,7 @@ type Reconstructor interface {
|
||||
ctx context.Context, blindedBlocks []interfaces.ReadOnlySignedBeaconBlock,
|
||||
) ([]interfaces.SignedBeaconBlock, error)
|
||||
ReconstructBlobSidecars(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock, blockRoot [fieldparams.RootLength]byte, hi func(uint64) bool) ([]blocks.VerifiedROBlob, error)
|
||||
ConstructDataColumnSidecars(ctx context.Context, populator peerdas.ConstructionPopulator) ([]blocks.VerifiedRODataColumn, error)
|
||||
ConstructDataColumnSidecars(ctx context.Context, populator peerdas.ConstructionPopulator) ([]blocks.VerifiedRODataColumn, []blocks.PartialDataColumn, error)
|
||||
}
|
||||
|
||||
// EngineCaller defines a client that can interact with an Ethereum
|
||||
@@ -360,24 +354,6 @@ func (s *Service) ExchangeCapabilities(ctx context.Context) ([]string, error) {
|
||||
return elSupportedEndpointsSlice, nil
|
||||
}
|
||||
|
||||
// GetClientVersion calls engine_getClientVersionV1 to retrieve EL client information.
|
||||
func (s *Service) GetClientVersion(ctx context.Context) ([]ClientVersionV1, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "powchain.engine-api-client.GetClientVersion")
|
||||
defer span.End()
|
||||
|
||||
// Per spec, we send our own client info as the parameter
|
||||
clVersion := ClientVersionV1{
|
||||
Code: CLCode,
|
||||
Name: Name,
|
||||
Version: version.SemanticVersion(),
|
||||
Commit: version.GetCommitPrefix(),
|
||||
}
|
||||
|
||||
var result []ClientVersionV1
|
||||
err := s.rpcClient.CallContext(ctx, &result, GetClientVersionMethod, clVersion)
|
||||
return result, handleRPCError(err)
|
||||
}
|
||||
|
||||
// GetTerminalBlockHash returns the valid terminal block hash based on total difficulty.
|
||||
//
|
||||
// Spec code:
|
||||
@@ -581,6 +557,22 @@ func (s *Service) GetBlobsV2(ctx context.Context, versionedHashes []common.Hash)
|
||||
return result, handleRPCError(err)
|
||||
}
|
||||
|
||||
func (s *Service) GetBlobsV3(ctx context.Context, versionedHashes []common.Hash) ([]*pb.BlobAndProofV2, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "powchain.engine-api-client.GetBlobsV3")
|
||||
defer span.End()
|
||||
start := time.Now()
|
||||
|
||||
if !s.capabilityCache.has(GetBlobsV3) {
|
||||
return nil, errors.New(fmt.Sprintf("%s is not supported", GetBlobsV3))
|
||||
}
|
||||
|
||||
getBlobsV3RequestsTotal.Inc()
|
||||
result := make([]*pb.BlobAndProofV2, len(versionedHashes))
|
||||
err := s.rpcClient.CallContext(ctx, &result, GetBlobsV3, versionedHashes)
|
||||
getBlobsV3Latency.Observe(time.Since(start).Seconds())
|
||||
return result, handleRPCError(err)
|
||||
}
|
||||
|
||||
// ReconstructFullBlock takes in a blinded beacon block and reconstructs
|
||||
// a beacon block with a full execution payload via the engine API.
|
||||
func (s *Service) ReconstructFullBlock(
|
||||
@@ -691,40 +683,47 @@ func (s *Service) ReconstructBlobSidecars(ctx context.Context, block interfaces.
|
||||
return verifiedBlobs, nil
|
||||
}
|
||||
|
||||
func (s *Service) ConstructDataColumnSidecars(ctx context.Context, populator peerdas.ConstructionPopulator) ([]blocks.VerifiedRODataColumn, error) {
|
||||
func (s *Service) ConstructDataColumnSidecars(ctx context.Context, populator peerdas.ConstructionPopulator) ([]blocks.VerifiedRODataColumn, []blocks.PartialDataColumn, error) {
|
||||
root := populator.Root()
|
||||
|
||||
// Fetch cells and proofs from the execution client using the KZG commitments from the sidecar.
|
||||
commitments, err := populator.Commitments()
|
||||
if err != nil {
|
||||
return nil, wrapWithBlockRoot(err, root, "commitments")
|
||||
return nil, nil, wrapWithBlockRoot(err, root, "commitments")
|
||||
}
|
||||
|
||||
cellsPerBlob, proofsPerBlob, err := s.fetchCellsAndProofsFromExecution(ctx, commitments)
|
||||
included, cellsPerBlob, proofsPerBlob, err := s.fetchCellsAndProofsFromExecution(ctx, commitments)
|
||||
log.Info("Received cells and proofs from execution client", "included", included, "cells count", len(cellsPerBlob), "err", err)
|
||||
if err != nil {
|
||||
return nil, wrapWithBlockRoot(err, root, "fetch cells and proofs from execution client")
|
||||
return nil, nil, wrapWithBlockRoot(err, root, "fetch cells and proofs from execution client")
|
||||
}
|
||||
|
||||
// Return early if nothing is returned from the EL.
|
||||
if len(cellsPerBlob) == 0 {
|
||||
return nil, nil
|
||||
partialColumns, err := peerdas.PartialColumns(included, cellsPerBlob, proofsPerBlob, populator)
|
||||
haveAllBlobs := included.Count() == uint64(len(commitments))
|
||||
log.Info("Constructed partial columns", "haveAllBlobs", haveAllBlobs)
|
||||
|
||||
if haveAllBlobs {
|
||||
// Construct data column sidears from the signed block and cells and proofs.
|
||||
roSidecars, err := peerdas.DataColumnSidecars(cellsPerBlob, proofsPerBlob, populator)
|
||||
if err != nil {
|
||||
return nil, nil, wrapWithBlockRoot(err, populator.Root(), "data column sidcars from column sidecar")
|
||||
}
|
||||
|
||||
// Upgrade the sidecars to verified sidecars.
|
||||
// We trust the execution layer we are connected to, so we can upgrade the sidecar into a verified one.
|
||||
verifiedROSidecars := upgradeSidecarsToVerifiedSidecars(roSidecars)
|
||||
|
||||
return verifiedROSidecars, partialColumns, nil
|
||||
}
|
||||
|
||||
// Construct data column sidears from the signed block and cells and proofs.
|
||||
roSidecars, err := peerdas.DataColumnSidecars(cellsPerBlob, proofsPerBlob, populator)
|
||||
if err != nil {
|
||||
return nil, wrapWithBlockRoot(err, populator.Root(), "data column sidcars from column sidecar")
|
||||
return nil, nil, wrapWithBlockRoot(err, populator.Root(), "partial columns from column sidecar")
|
||||
}
|
||||
|
||||
// Upgrade the sidecars to verified sidecars.
|
||||
// We trust the execution layer we are connected to, so we can upgrade the sidecar into a verified one.
|
||||
verifiedROSidecars := upgradeSidecarsToVerifiedSidecars(roSidecars)
|
||||
|
||||
return verifiedROSidecars, nil
|
||||
return nil, partialColumns, nil
|
||||
}
|
||||
|
||||
// fetchCellsAndProofsFromExecution fetches cells and proofs from the execution client (using engine_getBlobsV2 execution API method)
|
||||
func (s *Service) fetchCellsAndProofsFromExecution(ctx context.Context, kzgCommitments [][]byte) ([][]kzg.Cell, [][]kzg.Proof, error) {
|
||||
func (s *Service) fetchCellsAndProofsFromExecution(ctx context.Context, kzgCommitments [][]byte) (bitfield.Bitlist /* included parts */, [][]kzg.Cell, [][]kzg.Proof, error) {
|
||||
// Collect KZG hashes for all blobs.
|
||||
versionedHashes := make([]common.Hash, 0, len(kzgCommitments))
|
||||
for _, commitment := range kzgCommitments {
|
||||
@@ -732,24 +731,34 @@ func (s *Service) fetchCellsAndProofsFromExecution(ctx context.Context, kzgCommi
|
||||
versionedHashes = append(versionedHashes, versionedHash)
|
||||
}
|
||||
|
||||
var blobAndProofs []*pb.BlobAndProofV2
|
||||
|
||||
// Fetch all blobsAndCellsProofs from the execution client.
|
||||
blobAndProofV2s, err := s.GetBlobsV2(ctx, versionedHashes)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "get blobs V2")
|
||||
var err error
|
||||
useV3 := s.capabilityCache.has(GetBlobsV3)
|
||||
if useV3 {
|
||||
// v3 can return a partial response. V2 is all or nothing
|
||||
blobAndProofs, err = s.GetBlobsV3(ctx, versionedHashes)
|
||||
} else {
|
||||
blobAndProofs, err = s.GetBlobsV2(ctx, versionedHashes)
|
||||
}
|
||||
|
||||
// Return early if nothing is returned from the EL.
|
||||
if len(blobAndProofV2s) == 0 {
|
||||
return nil, nil, nil
|
||||
if err != nil {
|
||||
return nil, nil, nil, errors.Wrapf(err, "get blobs V2/3")
|
||||
}
|
||||
|
||||
// Compute cells and proofs from the blobs and cell proofs.
|
||||
cellsPerBlob, proofsPerBlob, err := peerdas.ComputeCellsAndProofsFromStructured(blobAndProofV2s)
|
||||
included, cellsPerBlob, proofsPerBlob, err := peerdas.ComputeCellsAndProofsFromStructured(uint64(len(kzgCommitments)), blobAndProofs)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "compute cells and proofs")
|
||||
return nil, nil, nil, errors.Wrap(err, "compute cells and proofs")
|
||||
}
|
||||
if included.Count() == uint64(len(kzgCommitments)) {
|
||||
getBlobsV3CompleteResponsesTotal.Inc()
|
||||
} else if included.Count() > 0 {
|
||||
getBlobsV3PartialResponsesTotal.Inc()
|
||||
}
|
||||
|
||||
return cellsPerBlob, proofsPerBlob, nil
|
||||
return included, cellsPerBlob, proofsPerBlob, nil
|
||||
}
|
||||
|
||||
// upgradeSidecarsToVerifiedSidecars upgrades a list of data column sidecars into verified data column sidecars.
|
||||
|
||||
@@ -2587,7 +2587,7 @@ func TestConstructDataColumnSidecars(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("GetBlobsV2 is not supported", func(t *testing.T) {
|
||||
_, err := client.ConstructDataColumnSidecars(ctx, peerdas.PopulateFromBlock(roBlock))
|
||||
_, _, err := client.ConstructDataColumnSidecars(ctx, peerdas.PopulateFromBlock(roBlock))
|
||||
require.ErrorContains(t, "engine_getBlobsV2 is not supported", err)
|
||||
})
|
||||
|
||||
@@ -2598,7 +2598,7 @@ func TestConstructDataColumnSidecars(t *testing.T) {
|
||||
rpcClient, client := setupRpcClientV2(t, srv.URL, client)
|
||||
defer rpcClient.Close()
|
||||
|
||||
dataColumns, err := client.ConstructDataColumnSidecars(ctx, peerdas.PopulateFromBlock(roBlock))
|
||||
dataColumns, _, err := client.ConstructDataColumnSidecars(ctx, peerdas.PopulateFromBlock(roBlock))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, len(dataColumns))
|
||||
})
|
||||
@@ -2611,7 +2611,7 @@ func TestConstructDataColumnSidecars(t *testing.T) {
|
||||
rpcClient, client := setupRpcClientV2(t, srv.URL, client)
|
||||
defer rpcClient.Close()
|
||||
|
||||
dataColumns, err := client.ConstructDataColumnSidecars(ctx, peerdas.PopulateFromBlock(roBlock))
|
||||
dataColumns, _, err := client.ConstructDataColumnSidecars(ctx, peerdas.PopulateFromBlock(roBlock))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 128, len(dataColumns))
|
||||
})
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/runtime/version"
|
||||
)
|
||||
|
||||
const (
|
||||
// CLCode is the two-letter client code for Prysm.
|
||||
CLCode = "PR"
|
||||
Name = "Prysm"
|
||||
)
|
||||
|
||||
// GraffitiInfo holds version information for generating block graffiti.
|
||||
// It is thread-safe and can be updated by the execution service and read by the validator server.
|
||||
type GraffitiInfo struct {
|
||||
mu sync.RWMutex
|
||||
elCode string // From engine_getClientVersionV1
|
||||
elCommit string // From engine_getClientVersionV1
|
||||
logOnce sync.Once
|
||||
}
|
||||
|
||||
// NewGraffitiInfo creates a new GraffitiInfo.
|
||||
func NewGraffitiInfo() *GraffitiInfo {
|
||||
return &GraffitiInfo{}
|
||||
}
|
||||
|
||||
// UpdateFromEngine updates the EL client information.
|
||||
func (g *GraffitiInfo) UpdateFromEngine(code, commit string) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.elCode = code
|
||||
g.elCommit = strings.TrimPrefix(commit, "0x")
|
||||
}
|
||||
|
||||
// GenerateGraffiti generates graffiti using the flexible standard
|
||||
// with the provided user graffiti from the validator client request.
|
||||
// It places user graffiti first, then appends as much client info as space allows.
|
||||
//
|
||||
// A space separator is added between user graffiti and client info when it
|
||||
// fits without reducing the client version tier.
|
||||
//
|
||||
// Available Space | Format
|
||||
// ≥13 bytes | user + space + EL(2)+commit(4)+CL(2)+commit(4) e.g. "Sushi GEabcdPRe4f6"
|
||||
// 12 bytes | user + EL(2)+commit(4)+CL(2)+commit(4) e.g. "12345678901234567890GEabcdPRe4f6"
|
||||
// 9-11 bytes | user + space + EL(2)+commit(2)+CL(2)+commit(2) e.g. "12345678901234567890123 GEabPRe4"
|
||||
// 8 bytes | user + EL(2)+commit(2)+CL(2)+commit(2) e.g. "123456789012345678901234GEabPRe4"
|
||||
// 5-7 bytes | user + space + EL(2)+CL(2) e.g. "123456789012345678901234567 GEPR"
|
||||
// 4 bytes | user + EL(2)+CL(2) e.g. "1234567890123456789012345678GEPR"
|
||||
// 3 bytes | user + space + code(2) e.g. "12345678901234567890123456789 GE"
|
||||
// 2 bytes | user + code(2) e.g. "123456789012345678901234567890GE"
|
||||
// <2 bytes | user only e.g. "1234567890123456789012345678901x"
|
||||
func (g *GraffitiInfo) GenerateGraffiti(userGraffiti []byte) [32]byte {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
|
||||
var result [32]byte
|
||||
userStr := string(userGraffiti)
|
||||
// Trim trailing null bytes
|
||||
for len(userStr) > 0 && userStr[len(userStr)-1] == 0 {
|
||||
userStr = userStr[:len(userStr)-1]
|
||||
}
|
||||
|
||||
available := 32 - len(userStr)
|
||||
|
||||
clCommit := version.GetCommitPrefix()
|
||||
clCommit4 := truncateCommit(clCommit, 4)
|
||||
clCommit2 := truncateCommit(clCommit, 2)
|
||||
|
||||
// If no EL info, clear EL commits but still include CL info
|
||||
var elCommit4, elCommit2 string
|
||||
if g.elCode != "" {
|
||||
elCommit4 = truncateCommit(g.elCommit, 4)
|
||||
elCommit2 = truncateCommit(g.elCommit, 2)
|
||||
}
|
||||
|
||||
// Add a space separator between user graffiti and client info,
|
||||
// but only if it won't reduce the space available for client version info.
|
||||
space := func(minForTier int) string {
|
||||
if len(userStr) > 0 && available >= minForTier+1 {
|
||||
return " "
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var graffiti string
|
||||
switch {
|
||||
case available >= 12:
|
||||
// Full: user+EL(2)+commit(4)+CL(2)+commit(4)
|
||||
graffiti = userStr + space(12) + g.elCode + elCommit4 + CLCode + clCommit4
|
||||
case available >= 8:
|
||||
// Reduced commits: user+EL(2)+commit(2)+CL(2)+commit(2)
|
||||
graffiti = userStr + space(8) + g.elCode + elCommit2 + CLCode + clCommit2
|
||||
case available >= 4:
|
||||
// Codes only: user+EL(2)+CL(2)
|
||||
graffiti = userStr + space(4) + g.elCode + CLCode
|
||||
case available >= 2:
|
||||
// Single code: user+code(2)
|
||||
if g.elCode != "" {
|
||||
graffiti = userStr + space(2) + g.elCode
|
||||
} else {
|
||||
graffiti = userStr + space(2) + CLCode
|
||||
}
|
||||
default:
|
||||
// User graffiti only
|
||||
graffiti = userStr
|
||||
}
|
||||
|
||||
g.logOnce.Do(func() {
|
||||
logGraffitiInfo(graffiti, available)
|
||||
})
|
||||
|
||||
copy(result[:], graffiti)
|
||||
return result
|
||||
}
|
||||
|
||||
// logGraffitiInfo logs the graffiti that will be used.
|
||||
func logGraffitiInfo(graffiti string, available int) {
|
||||
if available >= 2 {
|
||||
log.WithField("graffiti", graffiti).Info("Graffiti includes client version info appended after user graffiti")
|
||||
return
|
||||
}
|
||||
log.WithField("graffiti", graffiti).Info("Prysm adds consensus and execution debugging information to the end of the graffiti field when possible. To prevent deletion of debugging info, please consider using a shorter graffiti")
|
||||
}
|
||||
|
||||
// truncateCommit returns the first n characters of the commit string.
|
||||
func truncateCommit(commit string, n int) string {
|
||||
if len(commit) <= n {
|
||||
return commit
|
||||
}
|
||||
return commit[:n]
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/testing/require"
|
||||
)
|
||||
|
||||
func TestGraffitiInfo_GenerateGraffiti(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
elCode string
|
||||
elCommit string
|
||||
userGraffiti []byte
|
||||
wantPrefix string // user graffiti appears first
|
||||
wantSuffix string // client version info appended after
|
||||
}{
|
||||
// No EL info cases (CL info "PR" + commit still included when space allows)
|
||||
{
|
||||
name: "No EL - empty user graffiti",
|
||||
elCode: "",
|
||||
elCommit: "",
|
||||
userGraffiti: []byte{},
|
||||
wantPrefix: "PR", // Only CL code + commit (no user graffiti to prefix)
|
||||
},
|
||||
{
|
||||
name: "No EL - short user graffiti",
|
||||
elCode: "",
|
||||
elCommit: "",
|
||||
userGraffiti: []byte("my validator"),
|
||||
wantPrefix: "my validator",
|
||||
wantSuffix: " PR", // space + CL code appended
|
||||
},
|
||||
{
|
||||
name: "No EL - 28 char user graffiti (4 bytes available)",
|
||||
elCode: "",
|
||||
elCommit: "",
|
||||
userGraffiti: []byte("1234567890123456789012345678"), // 28 chars, 4 bytes available = codes only
|
||||
wantPrefix: "1234567890123456789012345678",
|
||||
wantSuffix: "PR", // CL code (no EL, so just PR)
|
||||
},
|
||||
{
|
||||
name: "No EL - 30 char user graffiti (2 bytes available)",
|
||||
elCode: "",
|
||||
elCommit: "",
|
||||
userGraffiti: []byte("123456789012345678901234567890"), // 30 chars, 2 bytes available = fits PR
|
||||
wantPrefix: "123456789012345678901234567890",
|
||||
wantSuffix: "PR",
|
||||
},
|
||||
{
|
||||
name: "No EL - 31 char user graffiti (1 byte available)",
|
||||
elCode: "",
|
||||
elCommit: "",
|
||||
userGraffiti: []byte("1234567890123456789012345678901"), // 31 chars, 1 byte available = not enough for code
|
||||
wantPrefix: "1234567890123456789012345678901", // User only
|
||||
},
|
||||
{
|
||||
name: "No EL - 32 char user graffiti (0 bytes available)",
|
||||
elCode: "",
|
||||
elCommit: "",
|
||||
userGraffiti: []byte("12345678901234567890123456789012"),
|
||||
wantPrefix: "12345678901234567890123456789012", // User only
|
||||
},
|
||||
// With EL info - flexible standard format cases
|
||||
{
|
||||
name: "With EL - full format (empty user graffiti)",
|
||||
elCode: "GE",
|
||||
elCommit: "abcd1234",
|
||||
userGraffiti: []byte{},
|
||||
wantPrefix: "GEabcdPR", // No user graffiti, starts with client info
|
||||
},
|
||||
{
|
||||
name: "With EL - full format (short user graffiti)",
|
||||
elCode: "GE",
|
||||
elCommit: "abcd1234",
|
||||
userGraffiti: []byte("Bob"),
|
||||
wantPrefix: "Bob",
|
||||
wantSuffix: " GEabcdPR", // space + EL(2)+commit(4)+CL(2)+commit(4)
|
||||
},
|
||||
{
|
||||
name: "With EL - full format (20 char user, 12 bytes available) - no space, would reduce tier",
|
||||
elCode: "GE",
|
||||
elCommit: "abcd1234",
|
||||
userGraffiti: []byte("12345678901234567890"), // 20 chars, leaves exactly 12 bytes = full format, no room for space
|
||||
wantPrefix: "12345678901234567890",
|
||||
wantSuffix: "GEabcdPR",
|
||||
},
|
||||
{
|
||||
name: "With EL - full format (19 char user, 13 bytes available) - space fits",
|
||||
elCode: "GE",
|
||||
elCommit: "abcd1234",
|
||||
userGraffiti: []byte("1234567890123456789"), // 19 chars, leaves 13 bytes = full format + space
|
||||
wantPrefix: "1234567890123456789",
|
||||
wantSuffix: " GEabcdPR",
|
||||
},
|
||||
{
|
||||
name: "With EL - reduced commits (24 char user, 8 bytes available) - no space, would reduce tier",
|
||||
elCode: "GE",
|
||||
elCommit: "abcd1234",
|
||||
userGraffiti: []byte("123456789012345678901234"), // 24 chars, leaves exactly 8 bytes = reduced format, no room for space
|
||||
wantPrefix: "123456789012345678901234",
|
||||
wantSuffix: "GEabPR",
|
||||
},
|
||||
{
|
||||
name: "With EL - reduced commits (23 char user, 9 bytes available) - space fits",
|
||||
elCode: "GE",
|
||||
elCommit: "abcd1234",
|
||||
userGraffiti: []byte("12345678901234567890123"), // 23 chars, leaves 9 bytes = reduced format + space
|
||||
wantPrefix: "12345678901234567890123",
|
||||
wantSuffix: " GEabPR",
|
||||
},
|
||||
{
|
||||
name: "With EL - codes only (28 char user, 4 bytes available) - no space, would reduce tier",
|
||||
elCode: "GE",
|
||||
elCommit: "abcd1234",
|
||||
userGraffiti: []byte("1234567890123456789012345678"), // 28 chars, leaves exactly 4 bytes = codes only, no room for space
|
||||
wantPrefix: "1234567890123456789012345678",
|
||||
wantSuffix: "GEPR",
|
||||
},
|
||||
{
|
||||
name: "With EL - codes only (27 char user, 5 bytes available) - space fits",
|
||||
elCode: "GE",
|
||||
elCommit: "abcd1234",
|
||||
userGraffiti: []byte("123456789012345678901234567"), // 27 chars, leaves 5 bytes = codes only + space
|
||||
wantPrefix: "123456789012345678901234567",
|
||||
wantSuffix: " GEPR",
|
||||
},
|
||||
{
|
||||
name: "With EL - EL code only (30 char user, 2 bytes available) - no space, would reduce tier",
|
||||
elCode: "GE",
|
||||
elCommit: "abcd1234",
|
||||
userGraffiti: []byte("123456789012345678901234567890"), // 30 chars, leaves exactly 2 bytes = EL code only, no room for space
|
||||
wantPrefix: "123456789012345678901234567890",
|
||||
wantSuffix: "GE",
|
||||
},
|
||||
{
|
||||
name: "With EL - EL code only (29 char user, 3 bytes available) - space fits",
|
||||
elCode: "GE",
|
||||
elCommit: "abcd1234",
|
||||
userGraffiti: []byte("12345678901234567890123456789"), // 29 chars, leaves 3 bytes = EL code + space
|
||||
wantPrefix: "12345678901234567890123456789",
|
||||
wantSuffix: " GE",
|
||||
},
|
||||
{
|
||||
name: "With EL - user only (31 char user, 1 byte available)",
|
||||
elCode: "GE",
|
||||
elCommit: "abcd1234",
|
||||
userGraffiti: []byte("1234567890123456789012345678901"), // 31 chars, leaves 1 byte = not enough for code
|
||||
wantPrefix: "1234567890123456789012345678901", // User only
|
||||
},
|
||||
{
|
||||
name: "With EL - user only (32 char user, 0 bytes available)",
|
||||
elCode: "GE",
|
||||
elCommit: "abcd1234",
|
||||
userGraffiti: []byte("12345678901234567890123456789012"),
|
||||
wantPrefix: "12345678901234567890123456789012",
|
||||
},
|
||||
// Null byte handling
|
||||
{
|
||||
name: "Null bytes - input with trailing nulls",
|
||||
elCode: "GE",
|
||||
elCommit: "abcd1234",
|
||||
userGraffiti: append([]byte("test"), 0, 0, 0),
|
||||
wantPrefix: "test",
|
||||
wantSuffix: " GEabcdPR",
|
||||
},
|
||||
// 0x prefix handling - some ELs return 0x-prefixed commits
|
||||
{
|
||||
name: "0x prefix - stripped from EL commit",
|
||||
elCode: "GE",
|
||||
elCommit: "0xabcd1234",
|
||||
userGraffiti: []byte{},
|
||||
wantPrefix: "GEabcdPR",
|
||||
},
|
||||
{
|
||||
name: "No 0x prefix - commit used as-is",
|
||||
elCode: "NM",
|
||||
elCommit: "abcd1234",
|
||||
userGraffiti: []byte{},
|
||||
wantPrefix: "NMabcdPR",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewGraffitiInfo()
|
||||
if tt.elCode != "" {
|
||||
g.UpdateFromEngine(tt.elCode, tt.elCommit)
|
||||
}
|
||||
|
||||
result := g.GenerateGraffiti(tt.userGraffiti)
|
||||
resultStr := string(result[:])
|
||||
trimmed := trimNullBytes(resultStr)
|
||||
|
||||
// Check prefix (user graffiti comes first)
|
||||
require.Equal(t, true, len(trimmed) >= len(tt.wantPrefix), "Result too short for prefix check")
|
||||
require.Equal(t, tt.wantPrefix, trimmed[:len(tt.wantPrefix)], "Prefix mismatch")
|
||||
|
||||
// Check suffix if specified (client version info appended)
|
||||
if tt.wantSuffix != "" {
|
||||
require.Equal(t, true, len(trimmed) >= len(tt.wantSuffix), "Result too short for suffix check")
|
||||
// The suffix should appear somewhere after the prefix
|
||||
afterPrefix := trimmed[len(tt.wantPrefix):]
|
||||
require.Equal(t, true, len(afterPrefix) >= len(tt.wantSuffix), "Not enough room for suffix after prefix")
|
||||
require.Equal(t, tt.wantSuffix, afterPrefix[:len(tt.wantSuffix)], "Suffix mismatch")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGraffitiInfo_UpdateFromEngine(t *testing.T) {
|
||||
g := NewGraffitiInfo()
|
||||
|
||||
// Initially no EL info - should still have CL info (PR + commit)
|
||||
result := g.GenerateGraffiti([]byte{})
|
||||
resultStr := trimNullBytes(string(result[:]))
|
||||
require.Equal(t, "PR", resultStr[:2], "Expected CL info before update")
|
||||
|
||||
// Update with EL info
|
||||
g.UpdateFromEngine("GE", "1234abcd")
|
||||
|
||||
result = g.GenerateGraffiti([]byte{})
|
||||
resultStr = trimNullBytes(string(result[:]))
|
||||
require.Equal(t, "GE1234PR", resultStr[:8], "Expected EL+CL info after update")
|
||||
}
|
||||
|
||||
func TestTruncateCommit(t *testing.T) {
|
||||
tests := []struct {
|
||||
commit string
|
||||
n int
|
||||
want string
|
||||
}{
|
||||
{"abcd1234", 4, "abcd"},
|
||||
{"ab", 4, "ab"},
|
||||
{"", 4, ""},
|
||||
{"abcdef", 2, "ab"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := truncateCommit(tt.commit, tt.n)
|
||||
require.Equal(t, tt.want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func trimNullBytes(s string) string {
|
||||
for len(s) > 0 && s[len(s)-1] == 0 {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -34,6 +34,25 @@ var (
|
||||
Buckets: []float64{25, 50, 100, 200, 500, 1000, 2000, 4000},
|
||||
},
|
||||
)
|
||||
getBlobsV3RequestsTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "beacon_engine_getBlobsV3_requests_total",
|
||||
Help: "Total number of engine_getBlobsV3 requests sent",
|
||||
})
|
||||
getBlobsV3CompleteResponsesTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "beacon_engine_getBlobsV3_complete_responses_total",
|
||||
Help: "Total number of complete engine_getBlobsV3 successful responses received",
|
||||
})
|
||||
getBlobsV3PartialResponsesTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "beacon_engine_getBlobsV3_partial_responses_total",
|
||||
Help: "Total number of engine_getBlobsV3 partial responses received",
|
||||
})
|
||||
getBlobsV3Latency = promauto.NewHistogram(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "beacon_engine_getBlobsV3_request_duration_seconds",
|
||||
Help: "Duration of engine_getBlobsV3 requests in seconds",
|
||||
Buckets: []float64{0.025, 0.05, 0.1, 0.2, 0.5, 1, 2, 4},
|
||||
},
|
||||
)
|
||||
errParseCount = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "execution_parse_error_count",
|
||||
Help: "The number of errors that occurred while parsing execution payload",
|
||||
|
||||
@@ -124,11 +124,3 @@ func WithVerifierWaiter(v *verification.InitializerWaiter) Option {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithGraffitiInfo sets the GraffitiInfo for client version tracking.
|
||||
func WithGraffitiInfo(g *GraffitiInfo) Option {
|
||||
return func(s *Service) error {
|
||||
s.graffitiInfo = g
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +162,6 @@ type Service struct {
|
||||
verifierWaiter *verification.InitializerWaiter
|
||||
blobVerifier verification.NewBlobVerifier
|
||||
capabilityCache *capabilityCache
|
||||
graffitiInfo *GraffitiInfo
|
||||
}
|
||||
|
||||
// NewService sets up a new instance with an ethclient when given a web3 endpoint as a string in the config.
|
||||
@@ -319,28 +318,6 @@ func (s *Service) updateConnectedETH1(state bool) {
|
||||
s.updateBeaconNodeStats()
|
||||
}
|
||||
|
||||
// GraffitiInfo returns the GraffitiInfo struct for graffiti generation.
|
||||
func (s *Service) GraffitiInfo() *GraffitiInfo {
|
||||
return s.graffitiInfo
|
||||
}
|
||||
|
||||
// updateGraffitiInfo fetches EL client version and updates the graffiti info.
|
||||
func (s *Service) updateGraffitiInfo() {
|
||||
if s.graffitiInfo == nil {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(s.ctx, time.Second)
|
||||
defer cancel()
|
||||
versions, err := s.GetClientVersion(ctx)
|
||||
if err != nil {
|
||||
log.WithError(err).Debug("Could not get execution client version for graffiti")
|
||||
return
|
||||
}
|
||||
if len(versions) >= 1 {
|
||||
s.graffitiInfo.UpdateFromEngine(versions[0].Code, versions[0].Commit)
|
||||
}
|
||||
}
|
||||
|
||||
// refers to the latest eth1 block which follows the condition: eth1_timestamp +
|
||||
// SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE <= current_unix_time
|
||||
func (s *Service) followedBlockHeight(ctx context.Context) (uint64, error) {
|
||||
@@ -621,12 +598,6 @@ func (s *Service) run(done <-chan struct{}) {
|
||||
chainstartTicker := time.NewTicker(logPeriod)
|
||||
defer chainstartTicker.Stop()
|
||||
|
||||
// Update graffiti info 4 times per epoch (~96 seconds with 12s slots and 32 slots/epoch)
|
||||
graffitiTicker := time.NewTicker(96 * time.Second)
|
||||
defer graffitiTicker.Stop()
|
||||
// Initial update
|
||||
s.updateGraffitiInfo()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
@@ -651,8 +622,6 @@ func (s *Service) run(done <-chan struct{}) {
|
||||
continue
|
||||
}
|
||||
s.logTillChainStart(context.Background())
|
||||
case <-graffitiTicker.C:
|
||||
s.updateGraffitiInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,8 +118,8 @@ func (e *EngineClient) ReconstructBlobSidecars(context.Context, interfaces.ReadO
|
||||
}
|
||||
|
||||
// ConstructDataColumnSidecars is a mock implementation of the ConstructDataColumnSidecars method.
|
||||
func (e *EngineClient) ConstructDataColumnSidecars(context.Context, peerdas.ConstructionPopulator) ([]blocks.VerifiedRODataColumn, error) {
|
||||
return e.DataColumnSidecars, e.ErrorDataColumnSidecars
|
||||
func (e *EngineClient) ConstructDataColumnSidecars(context.Context, peerdas.ConstructionPopulator) ([]blocks.VerifiedRODataColumn, []blocks.PartialDataColumn, error) {
|
||||
return e.DataColumnSidecars, nil, e.ErrorDataColumnSidecars
|
||||
}
|
||||
|
||||
// GetTerminalBlockHash --
|
||||
|
||||
@@ -6,7 +6,7 @@ go_library(
|
||||
"doc.go",
|
||||
"errors.go",
|
||||
"forkchoice.go",
|
||||
"gloas.go",
|
||||
"last_root.go",
|
||||
"log.go",
|
||||
"metrics.go",
|
||||
"node.go",
|
||||
@@ -33,7 +33,6 @@ go_library(
|
||||
"//config/params:go_default_library",
|
||||
"//consensus-types/blocks:go_default_library",
|
||||
"//consensus-types/forkchoice:go_default_library",
|
||||
"//consensus-types/interfaces:go_default_library",
|
||||
"//consensus-types/primitives:go_default_library",
|
||||
"//encoding/bytesutil:go_default_library",
|
||||
"//monitoring/tracing/trace:go_default_library",
|
||||
@@ -52,6 +51,7 @@ go_test(
|
||||
srcs = [
|
||||
"ffg_update_test.go",
|
||||
"forkchoice_test.go",
|
||||
"last_root_test.go",
|
||||
"no_vote_test.go",
|
||||
"node_test.go",
|
||||
"on_tick_test.go",
|
||||
|
||||
@@ -31,8 +31,8 @@ func New() *ForkChoice {
|
||||
prevJustifiedCheckpoint: &forkchoicetypes.Checkpoint{},
|
||||
finalizedCheckpoint: &forkchoicetypes.Checkpoint{},
|
||||
proposerBoostRoot: [32]byte{},
|
||||
emptyNodeByRoot: make(map[[fieldparams.RootLength]byte]*PayloadNode),
|
||||
fullNodeByRoot: make(map[[fieldparams.RootLength]byte]*PayloadNode),
|
||||
nodeByRoot: make(map[[fieldparams.RootLength]byte]*Node),
|
||||
nodeByPayload: make(map[[fieldparams.RootLength]byte]*Node),
|
||||
slashedIndices: make(map[primitives.ValidatorIndex]bool),
|
||||
receivedBlocksLastEpoch: [fieldparams.SlotsPerEpoch]primitives.Slot{},
|
||||
}
|
||||
@@ -44,7 +44,7 @@ func New() *ForkChoice {
|
||||
|
||||
// NodeCount returns the current number of nodes in the Store.
|
||||
func (f *ForkChoice) NodeCount() int {
|
||||
return len(f.store.emptyNodeByRoot)
|
||||
return len(f.store.nodeByRoot)
|
||||
}
|
||||
|
||||
// Head returns the head root from fork choice store.
|
||||
@@ -65,14 +65,14 @@ func (f *ForkChoice) Head(
|
||||
return [32]byte{}, errors.Wrap(err, "could not apply proposer boost score")
|
||||
}
|
||||
|
||||
if err := f.store.applyWeightChangesConsensusNode(ctx, f.store.treeRootNode); err != nil {
|
||||
if err := f.store.treeRootNode.applyWeightChanges(ctx); err != nil {
|
||||
return [32]byte{}, errors.Wrap(err, "could not apply weight changes")
|
||||
}
|
||||
|
||||
jc := f.JustifiedCheckpoint()
|
||||
fc := f.FinalizedCheckpoint()
|
||||
currentEpoch := slots.EpochsSinceGenesis(f.store.genesisTime)
|
||||
if err := f.store.updateBestDescendantConsensusNode(ctx, f.store.treeRootNode, jc.Epoch, fc.Epoch, currentEpoch); err != nil {
|
||||
if err := f.store.treeRootNode.updateBestDescendant(ctx, jc.Epoch, fc.Epoch, currentEpoch); err != nil {
|
||||
return [32]byte{}, errors.Wrap(err, "could not update best descendant")
|
||||
}
|
||||
return f.store.head(ctx)
|
||||
@@ -119,14 +119,14 @@ func (f *ForkChoice) InsertNode(ctx context.Context, state state.BeaconState, ro
|
||||
return errInvalidNilCheckpoint
|
||||
}
|
||||
finalizedEpoch := fc.Epoch
|
||||
pn, err := f.store.insert(ctx, roblock, justifiedEpoch, finalizedEpoch)
|
||||
node, err := f.store.insert(ctx, roblock, justifiedEpoch, finalizedEpoch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jc, fc = f.store.pullTips(state, pn.node, jc, fc)
|
||||
jc, fc = f.store.pullTips(state, node, jc, fc)
|
||||
if err := f.updateCheckpoints(ctx, jc, fc); err != nil {
|
||||
_, remErr := f.store.removeNode(ctx, pn)
|
||||
_, remErr := f.store.removeNode(ctx, node)
|
||||
if remErr != nil {
|
||||
log.WithError(remErr).Error("Could not remove node")
|
||||
}
|
||||
@@ -149,63 +149,49 @@ func (f *ForkChoice) updateCheckpoints(ctx context.Context, jc, fc *ethpb.Checkp
|
||||
if fc.Epoch <= f.store.finalizedCheckpoint.Epoch {
|
||||
return nil
|
||||
}
|
||||
f.store.finalizedCheckpoint = &forkchoicetypes.Checkpoint{
|
||||
Epoch: fc.Epoch,
|
||||
Root: bytesutil.ToBytes32(fc.Root),
|
||||
}
|
||||
f.store.finalizedCheckpoint = &forkchoicetypes.Checkpoint{Epoch: fc.Epoch,
|
||||
Root: bytesutil.ToBytes32(fc.Root)}
|
||||
return f.store.prune(ctx)
|
||||
}
|
||||
|
||||
// HasNode returns true if the node exists in fork choice store,
|
||||
// false else wise.
|
||||
func (f *ForkChoice) HasNode(root [32]byte) bool {
|
||||
_, ok := f.store.emptyNodeByRoot[root]
|
||||
_, ok := f.store.nodeByRoot[root]
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsCanonical returns true if the given root is part of the canonical chain.
|
||||
func (f *ForkChoice) IsCanonical(root [32]byte) bool {
|
||||
// It is fine to pick empty node here since we only check if the beacon block is canonical.
|
||||
pn, ok := f.store.emptyNodeByRoot[root]
|
||||
if !ok || pn == nil {
|
||||
node, ok := f.store.nodeByRoot[root]
|
||||
if !ok || node == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if pn.node.bestDescendant == nil {
|
||||
// The node doesn't have any children
|
||||
if node.bestDescendant == nil {
|
||||
if f.store.headNode.bestDescendant == nil {
|
||||
// headNode is itself head.
|
||||
return pn.node == f.store.headNode
|
||||
return node == f.store.headNode
|
||||
}
|
||||
// headNode is not actualized and there are some descendants
|
||||
return pn.node == f.store.headNode.bestDescendant
|
||||
return node == f.store.headNode.bestDescendant
|
||||
}
|
||||
// The node has children
|
||||
if f.store.headNode.bestDescendant == nil {
|
||||
return pn.node.bestDescendant == f.store.headNode
|
||||
return node.bestDescendant == f.store.headNode
|
||||
}
|
||||
return pn.node.bestDescendant == f.store.headNode.bestDescendant
|
||||
return node.bestDescendant == f.store.headNode.bestDescendant
|
||||
}
|
||||
|
||||
// IsOptimistic returns true if the given root has been optimistically synced.
|
||||
// TODO: Gloas, the current implementation uses the result of the full block for
|
||||
// the given root. In gloas this would be incorrect and we should specify the
|
||||
// payload content, thus we should expose a full/empty version of this call.
|
||||
func (f *ForkChoice) IsOptimistic(root [32]byte) (bool, error) {
|
||||
if f.store.allTipsAreInvalid {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
en, ok := f.store.emptyNodeByRoot[root]
|
||||
if !ok || en == nil {
|
||||
node, ok := f.store.nodeByRoot[root]
|
||||
if !ok || node == nil {
|
||||
return true, ErrNilNode
|
||||
}
|
||||
fn := f.store.fullNodeByRoot[root]
|
||||
if fn != nil {
|
||||
return fn.optimistic, nil
|
||||
}
|
||||
|
||||
return en.optimistic, nil
|
||||
return node.optimistic, nil
|
||||
}
|
||||
|
||||
// AncestorRoot returns the ancestor root of input block root at a given slot.
|
||||
@@ -213,21 +199,17 @@ func (f *ForkChoice) AncestorRoot(ctx context.Context, root [32]byte, slot primi
|
||||
ctx, span := trace.StartSpan(ctx, "doublyLinkedForkchoice.AncestorRoot")
|
||||
defer span.End()
|
||||
|
||||
pn, ok := f.store.emptyNodeByRoot[root]
|
||||
if !ok || pn == nil {
|
||||
node, ok := f.store.nodeByRoot[root]
|
||||
if !ok || node == nil {
|
||||
return [32]byte{}, errors.Wrap(ErrNilNode, "could not determine ancestor root")
|
||||
}
|
||||
|
||||
n := pn.node
|
||||
for n.slot > slot {
|
||||
n := node
|
||||
for n != nil && n.slot > slot {
|
||||
if ctx.Err() != nil {
|
||||
return [32]byte{}, ctx.Err()
|
||||
}
|
||||
if n.parent == nil {
|
||||
n = nil
|
||||
break
|
||||
}
|
||||
n = n.parent.node
|
||||
n = n.parent
|
||||
}
|
||||
|
||||
if n == nil {
|
||||
@@ -240,11 +222,10 @@ func (f *ForkChoice) AncestorRoot(ctx context.Context, root [32]byte, slot primi
|
||||
// IsViableForCheckpoint returns whether the root passed is a checkpoint root for any
|
||||
// known chain in forkchoice.
|
||||
func (f *ForkChoice) IsViableForCheckpoint(cp *forkchoicetypes.Checkpoint) (bool, error) {
|
||||
pn, ok := f.store.emptyNodeByRoot[cp.Root]
|
||||
if !ok || pn == nil {
|
||||
node, ok := f.store.nodeByRoot[cp.Root]
|
||||
if !ok || node == nil {
|
||||
return false, nil
|
||||
}
|
||||
node := pn.node
|
||||
epochStart, err := slots.EpochStart(cp.Epoch)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -253,13 +234,10 @@ func (f *ForkChoice) IsViableForCheckpoint(cp *forkchoicetypes.Checkpoint) (bool
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// If it's the start of the epoch, it is a checkpoint
|
||||
if node.slot == epochStart {
|
||||
if len(node.children) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
// If there are no descendants of this beacon block, it is is viable as a checkpoint
|
||||
children := f.store.allConsensusChildren(node)
|
||||
if len(children) == 0 {
|
||||
if node.slot == epochStart {
|
||||
return true, nil
|
||||
}
|
||||
if !features.Get().IgnoreUnviableAttestations {
|
||||
@@ -269,8 +247,7 @@ func (f *ForkChoice) IsViableForCheckpoint(cp *forkchoicetypes.Checkpoint) (bool
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
// If some child is after the start of the epoch, the checkpoint is viable.
|
||||
for _, child := range children {
|
||||
for _, child := range node.children {
|
||||
if child.slot > epochStart {
|
||||
return true, nil
|
||||
}
|
||||
@@ -311,7 +288,7 @@ func (f *ForkChoice) updateBalances() error {
|
||||
if vote.currentRoot != vote.nextRoot || oldBalance != newBalance {
|
||||
// Ignore the vote if the root is not in fork choice
|
||||
// store, that means we have not seen the block before.
|
||||
nextNode, ok := f.store.emptyNodeByRoot[vote.nextRoot]
|
||||
nextNode, ok := f.store.nodeByRoot[vote.nextRoot]
|
||||
if ok && vote.nextRoot != zHash {
|
||||
// Protection against nil node
|
||||
if nextNode == nil {
|
||||
@@ -320,7 +297,7 @@ func (f *ForkChoice) updateBalances() error {
|
||||
nextNode.balance += newBalance
|
||||
}
|
||||
|
||||
currentNode, ok := f.store.emptyNodeByRoot[vote.currentRoot]
|
||||
currentNode, ok := f.store.nodeByRoot[vote.currentRoot]
|
||||
if ok && vote.currentRoot != zHash {
|
||||
// Protection against nil node
|
||||
if currentNode == nil {
|
||||
@@ -361,13 +338,13 @@ func (f *ForkChoice) ProposerBoost() [fieldparams.RootLength]byte {
|
||||
return f.store.proposerBoost()
|
||||
}
|
||||
|
||||
// SetOptimisticToValid sets the node with the given root as a fully validated node. The payload for this root MUST have been processed.
|
||||
// SetOptimisticToValid sets the node with the given root as a fully validated node
|
||||
func (f *ForkChoice) SetOptimisticToValid(ctx context.Context, root [fieldparams.RootLength]byte) error {
|
||||
fn, ok := f.store.fullNodeByRoot[root]
|
||||
if !ok || fn == nil {
|
||||
node, ok := f.store.nodeByRoot[root]
|
||||
if !ok || node == nil {
|
||||
return errors.Wrap(ErrNilNode, "could not set node to valid")
|
||||
}
|
||||
return f.store.setNodeAndParentValidated(ctx, fn)
|
||||
return node.setNodeAndParentValidated(ctx)
|
||||
}
|
||||
|
||||
// PreviousJustifiedCheckpoint of fork choice store.
|
||||
@@ -386,8 +363,8 @@ func (f *ForkChoice) FinalizedCheckpoint() *forkchoicetypes.Checkpoint {
|
||||
}
|
||||
|
||||
// SetOptimisticToInvalid removes a block with an invalid execution payload from fork choice store
|
||||
func (f *ForkChoice) SetOptimisticToInvalid(ctx context.Context, root, parentRoot, parentHash, payloadHash [fieldparams.RootLength]byte) ([][32]byte, error) {
|
||||
return f.store.setOptimisticToInvalid(ctx, root, parentRoot, parentHash, payloadHash)
|
||||
func (f *ForkChoice) SetOptimisticToInvalid(ctx context.Context, root, parentRoot, payloadHash [fieldparams.RootLength]byte) ([][32]byte, error) {
|
||||
return f.store.setOptimisticToInvalid(ctx, root, parentRoot, payloadHash)
|
||||
}
|
||||
|
||||
// InsertSlashedIndex adds the given slashed validator index to the
|
||||
@@ -410,7 +387,7 @@ func (f *ForkChoice) InsertSlashedIndex(_ context.Context, index primitives.Vali
|
||||
return
|
||||
}
|
||||
|
||||
node, ok := f.store.emptyNodeByRoot[f.votes[index].currentRoot]
|
||||
node, ok := f.store.nodeByRoot[f.votes[index].currentRoot]
|
||||
if !ok || node == nil {
|
||||
return
|
||||
}
|
||||
@@ -445,30 +422,22 @@ func (f *ForkChoice) UpdateFinalizedCheckpoint(fc *forkchoicetypes.Checkpoint) e
|
||||
}
|
||||
|
||||
// CommonAncestor returns the common ancestor root and slot between the two block roots r1 and r2.
|
||||
// This is payload aware. Consider the following situation
|
||||
// [A,full] <--- [B, full] <---[C,pending]
|
||||
//
|
||||
// \---------[B, empty] <--[D, pending]
|
||||
//
|
||||
// Then even though C and D both descend from the beacon block B, their common ancestor is A.
|
||||
// Notice that also this function **requires** that the two roots are actually contending blocks! otherwise the
|
||||
// behavior is not defined.
|
||||
func (f *ForkChoice) CommonAncestor(ctx context.Context, r1 [32]byte, r2 [32]byte) ([32]byte, primitives.Slot, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "doublyLinkedForkchoice.CommonAncestorRoot")
|
||||
defer span.End()
|
||||
|
||||
en1, ok := f.store.emptyNodeByRoot[r1]
|
||||
if !ok || en1 == nil {
|
||||
n1, ok := f.store.nodeByRoot[r1]
|
||||
if !ok || n1 == nil {
|
||||
return [32]byte{}, 0, forkchoice.ErrUnknownCommonAncestor
|
||||
}
|
||||
|
||||
// Do nothing if the input roots are the same.
|
||||
if r1 == r2 {
|
||||
return r1, en1.node.slot, nil
|
||||
return r1, n1.slot, nil
|
||||
}
|
||||
|
||||
en2, ok := f.store.emptyNodeByRoot[r2]
|
||||
if !ok || en2 == nil {
|
||||
n2, ok := f.store.nodeByRoot[r2]
|
||||
if !ok || n2 == nil {
|
||||
return [32]byte{}, 0, forkchoice.ErrUnknownCommonAncestor
|
||||
}
|
||||
|
||||
@@ -476,23 +445,23 @@ func (f *ForkChoice) CommonAncestor(ctx context.Context, r1 [32]byte, r2 [32]byt
|
||||
if ctx.Err() != nil {
|
||||
return [32]byte{}, 0, ctx.Err()
|
||||
}
|
||||
if en1.node.slot > en2.node.slot {
|
||||
en1 = en1.node.parent
|
||||
if n1.slot > n2.slot {
|
||||
n1 = n1.parent
|
||||
// Reaches the end of the tree and unable to find common ancestor.
|
||||
// This should not happen at runtime as the finalized
|
||||
// node has to be a common ancestor
|
||||
if en1 == nil {
|
||||
if n1 == nil {
|
||||
return [32]byte{}, 0, forkchoice.ErrUnknownCommonAncestor
|
||||
}
|
||||
} else {
|
||||
en2 = en2.node.parent
|
||||
n2 = n2.parent
|
||||
// Reaches the end of the tree and unable to find common ancestor.
|
||||
if en2 == nil {
|
||||
if n2 == nil {
|
||||
return [32]byte{}, 0, forkchoice.ErrUnknownCommonAncestor
|
||||
}
|
||||
}
|
||||
if en1 == en2 {
|
||||
return en1.node.root, en1.node.slot, nil
|
||||
if n1 == n2 {
|
||||
return n1.root, n1.slot, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -539,17 +508,35 @@ func (f *ForkChoice) CachedHeadRoot() [32]byte {
|
||||
|
||||
// FinalizedPayloadBlockHash returns the hash of the payload at the finalized checkpoint
|
||||
func (f *ForkChoice) FinalizedPayloadBlockHash() [32]byte {
|
||||
return f.store.latestHashForRoot(f.FinalizedCheckpoint().Root)
|
||||
root := f.FinalizedCheckpoint().Root
|
||||
node, ok := f.store.nodeByRoot[root]
|
||||
if !ok || node == nil {
|
||||
// This should not happen
|
||||
return [32]byte{}
|
||||
}
|
||||
return node.payloadHash
|
||||
}
|
||||
|
||||
// JustifiedPayloadBlockHash returns the hash of the payload at the justified checkpoint
|
||||
func (f *ForkChoice) JustifiedPayloadBlockHash() [32]byte {
|
||||
return f.store.latestHashForRoot(f.JustifiedCheckpoint().Root)
|
||||
root := f.JustifiedCheckpoint().Root
|
||||
node, ok := f.store.nodeByRoot[root]
|
||||
if !ok || node == nil {
|
||||
// This should not happen
|
||||
return [32]byte{}
|
||||
}
|
||||
return node.payloadHash
|
||||
}
|
||||
|
||||
// UnrealizedJustifiedPayloadBlockHash returns the hash of the payload at the unrealized justified checkpoint
|
||||
func (f *ForkChoice) UnrealizedJustifiedPayloadBlockHash() [32]byte {
|
||||
return f.store.latestHashForRoot(f.store.unrealizedJustifiedCheckpoint.Root)
|
||||
root := f.store.unrealizedJustifiedCheckpoint.Root
|
||||
node, ok := f.store.nodeByRoot[root]
|
||||
if !ok || node == nil {
|
||||
// This should not happen
|
||||
return [32]byte{}
|
||||
}
|
||||
return node.payloadHash
|
||||
}
|
||||
|
||||
// ForkChoiceDump returns a full dump of forkchoice.
|
||||
@@ -573,7 +560,7 @@ func (f *ForkChoice) ForkChoiceDump(ctx context.Context) (*forkchoice2.Dump, err
|
||||
nodes := make([]*forkchoice2.Node, 0, f.NodeCount())
|
||||
var err error
|
||||
if f.store.treeRootNode != nil {
|
||||
nodes, err = f.store.nodeTreeDump(ctx, f.store.treeRootNode, nodes)
|
||||
nodes, err = f.store.treeRootNode.nodeTreeDump(ctx, nodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -602,7 +589,7 @@ func (f *ForkChoice) SetBalancesByRooter(handler forkchoice.BalancesByRooter) {
|
||||
|
||||
// Weight returns the weight of the given root if found on the store
|
||||
func (f *ForkChoice) Weight(root [32]byte) (uint64, error) {
|
||||
n, ok := f.store.emptyNodeByRoot[root]
|
||||
n, ok := f.store.nodeByRoot[root]
|
||||
if !ok || n == nil {
|
||||
return 0, ErrNilNode
|
||||
}
|
||||
@@ -630,11 +617,11 @@ func (f *ForkChoice) updateJustifiedBalances(ctx context.Context, root [32]byte)
|
||||
|
||||
// Slot returns the slot of the given root if it's known to forkchoice
|
||||
func (f *ForkChoice) Slot(root [32]byte) (primitives.Slot, error) {
|
||||
n, ok := f.store.emptyNodeByRoot[root]
|
||||
n, ok := f.store.nodeByRoot[root]
|
||||
if !ok || n == nil {
|
||||
return 0, ErrNilNode
|
||||
}
|
||||
return n.node.slot, nil
|
||||
return n.slot, nil
|
||||
}
|
||||
|
||||
// DependentRoot returns the last root of the epoch prior to the requested ecoch in the canonical chain.
|
||||
@@ -642,7 +629,7 @@ func (f *ForkChoice) DependentRoot(epoch primitives.Epoch) ([32]byte, error) {
|
||||
return f.DependentRootForEpoch(f.CachedHeadRoot(), epoch)
|
||||
}
|
||||
|
||||
// DependentRootForEpoch return the last root of the epoch prior to the requested epoch for the given root.
|
||||
// DependentRootForEpoch return the last root of the epoch prior to the requested ecoch for the given root.
|
||||
func (f *ForkChoice) DependentRootForEpoch(root [32]byte, epoch primitives.Epoch) ([32]byte, error) {
|
||||
tr, err := f.TargetRootForEpoch(root, epoch)
|
||||
if err != nil {
|
||||
@@ -651,18 +638,18 @@ func (f *ForkChoice) DependentRootForEpoch(root [32]byte, epoch primitives.Epoch
|
||||
if tr == [32]byte{} {
|
||||
return [32]byte{}, nil
|
||||
}
|
||||
en, ok := f.store.emptyNodeByRoot[tr]
|
||||
if !ok || en == nil {
|
||||
node, ok := f.store.nodeByRoot[tr]
|
||||
if !ok || node == nil {
|
||||
return [32]byte{}, ErrNilNode
|
||||
}
|
||||
if slots.ToEpoch(en.node.slot) >= epoch {
|
||||
if en.node.parent != nil {
|
||||
en = en.node.parent
|
||||
if slots.ToEpoch(node.slot) >= epoch {
|
||||
if node.parent != nil {
|
||||
node = node.parent
|
||||
} else {
|
||||
return f.store.finalizedDependentRoot, nil
|
||||
}
|
||||
}
|
||||
return en.node.root, nil
|
||||
return node.root, nil
|
||||
}
|
||||
|
||||
// TargetRootForEpoch returns the root of the target block for a given epoch.
|
||||
@@ -674,48 +661,46 @@ func (f *ForkChoice) DependentRootForEpoch(root [32]byte, epoch primitives.Epoch
|
||||
// which case we return the root of the checkpoint of the chain containing the
|
||||
// passed root, at the given epoch
|
||||
func (f *ForkChoice) TargetRootForEpoch(root [32]byte, epoch primitives.Epoch) ([32]byte, error) {
|
||||
n, ok := f.store.emptyNodeByRoot[root]
|
||||
n, ok := f.store.nodeByRoot[root]
|
||||
if !ok || n == nil {
|
||||
return [32]byte{}, ErrNilNode
|
||||
}
|
||||
node := n.node
|
||||
nodeEpoch := slots.ToEpoch(node.slot)
|
||||
nodeEpoch := slots.ToEpoch(n.slot)
|
||||
if epoch > nodeEpoch {
|
||||
return node.root, nil
|
||||
return n.root, nil
|
||||
}
|
||||
if node.target == nil {
|
||||
if n.target == nil {
|
||||
return [32]byte{}, nil
|
||||
}
|
||||
targetRoot := node.target.root
|
||||
targetRoot := n.target.root
|
||||
if epoch == nodeEpoch {
|
||||
return targetRoot, nil
|
||||
}
|
||||
targetNode, ok := f.store.emptyNodeByRoot[targetRoot]
|
||||
targetNode, ok := f.store.nodeByRoot[targetRoot]
|
||||
if !ok || targetNode == nil {
|
||||
return [32]byte{}, ErrNilNode
|
||||
}
|
||||
// If slot 0 was not missed we consider a previous block to go back at least one epoch
|
||||
if nodeEpoch == slots.ToEpoch(targetNode.node.slot) {
|
||||
targetNode = targetNode.node.parent
|
||||
if nodeEpoch == slots.ToEpoch(targetNode.slot) {
|
||||
targetNode = targetNode.parent
|
||||
if targetNode == nil {
|
||||
return [32]byte{}, ErrNilNode
|
||||
}
|
||||
}
|
||||
return f.TargetRootForEpoch(targetNode.node.root, epoch)
|
||||
return f.TargetRootForEpoch(targetNode.root, epoch)
|
||||
}
|
||||
|
||||
// ParentRoot returns the block root of the parent node if it is in forkchoice.
|
||||
// The exception is for the finalized checkpoint root which we return the zero
|
||||
// hash.
|
||||
func (f *ForkChoice) ParentRoot(root [32]byte) ([32]byte, error) {
|
||||
n, ok := f.store.emptyNodeByRoot[root]
|
||||
n, ok := f.store.nodeByRoot[root]
|
||||
if !ok || n == nil {
|
||||
return [32]byte{}, ErrNilNode
|
||||
}
|
||||
// Return the zero hash for the tree root
|
||||
parent := n.node.parent
|
||||
if parent == nil {
|
||||
if n.parent == nil {
|
||||
return [32]byte{}, nil
|
||||
}
|
||||
return parent.node.root, nil
|
||||
return n.parent.root, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package doublylinkedtree
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/forkchoice"
|
||||
@@ -103,9 +104,9 @@ func TestForkChoice_UpdateBalancesPositiveChange(t *testing.T) {
|
||||
f.justifiedBalances = []uint64{10, 20, 30}
|
||||
require.NoError(t, f.updateBalances())
|
||||
s := f.store
|
||||
assert.Equal(t, uint64(10), s.emptyNodeByRoot[indexToHash(1)].balance)
|
||||
assert.Equal(t, uint64(20), s.emptyNodeByRoot[indexToHash(2)].balance)
|
||||
assert.Equal(t, uint64(30), s.emptyNodeByRoot[indexToHash(3)].balance)
|
||||
assert.Equal(t, uint64(10), s.nodeByRoot[indexToHash(1)].balance)
|
||||
assert.Equal(t, uint64(20), s.nodeByRoot[indexToHash(2)].balance)
|
||||
assert.Equal(t, uint64(30), s.nodeByRoot[indexToHash(3)].balance)
|
||||
}
|
||||
|
||||
func TestForkChoice_UpdateBalancesNegativeChange(t *testing.T) {
|
||||
@@ -121,9 +122,9 @@ func TestForkChoice_UpdateBalancesNegativeChange(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, st, roblock))
|
||||
s := f.store
|
||||
s.emptyNodeByRoot[indexToHash(1)].balance = 100
|
||||
s.emptyNodeByRoot[indexToHash(2)].balance = 100
|
||||
s.emptyNodeByRoot[indexToHash(3)].balance = 100
|
||||
s.nodeByRoot[indexToHash(1)].balance = 100
|
||||
s.nodeByRoot[indexToHash(2)].balance = 100
|
||||
s.nodeByRoot[indexToHash(3)].balance = 100
|
||||
|
||||
f.balances = []uint64{100, 100, 100}
|
||||
f.votes = []Vote{
|
||||
@@ -134,9 +135,9 @@ func TestForkChoice_UpdateBalancesNegativeChange(t *testing.T) {
|
||||
|
||||
f.justifiedBalances = []uint64{10, 20, 30}
|
||||
require.NoError(t, f.updateBalances())
|
||||
assert.Equal(t, uint64(10), s.emptyNodeByRoot[indexToHash(1)].balance)
|
||||
assert.Equal(t, uint64(20), s.emptyNodeByRoot[indexToHash(2)].balance)
|
||||
assert.Equal(t, uint64(30), s.emptyNodeByRoot[indexToHash(3)].balance)
|
||||
assert.Equal(t, uint64(10), s.nodeByRoot[indexToHash(1)].balance)
|
||||
assert.Equal(t, uint64(20), s.nodeByRoot[indexToHash(2)].balance)
|
||||
assert.Equal(t, uint64(30), s.nodeByRoot[indexToHash(3)].balance)
|
||||
}
|
||||
|
||||
func TestForkChoice_UpdateBalancesUnderflow(t *testing.T) {
|
||||
@@ -152,9 +153,9 @@ func TestForkChoice_UpdateBalancesUnderflow(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, st, roblock))
|
||||
s := f.store
|
||||
s.emptyNodeByRoot[indexToHash(1)].balance = 100
|
||||
s.emptyNodeByRoot[indexToHash(2)].balance = 100
|
||||
s.emptyNodeByRoot[indexToHash(3)].balance = 100
|
||||
s.nodeByRoot[indexToHash(1)].balance = 100
|
||||
s.nodeByRoot[indexToHash(2)].balance = 100
|
||||
s.nodeByRoot[indexToHash(3)].balance = 100
|
||||
|
||||
f.balances = []uint64{125, 125, 125}
|
||||
f.votes = []Vote{
|
||||
@@ -165,9 +166,9 @@ func TestForkChoice_UpdateBalancesUnderflow(t *testing.T) {
|
||||
|
||||
f.justifiedBalances = []uint64{10, 20, 30}
|
||||
require.NoError(t, f.updateBalances())
|
||||
assert.Equal(t, uint64(0), s.emptyNodeByRoot[indexToHash(1)].balance)
|
||||
assert.Equal(t, uint64(0), s.emptyNodeByRoot[indexToHash(2)].balance)
|
||||
assert.Equal(t, uint64(5), s.emptyNodeByRoot[indexToHash(3)].balance)
|
||||
assert.Equal(t, uint64(0), s.nodeByRoot[indexToHash(1)].balance)
|
||||
assert.Equal(t, uint64(0), s.nodeByRoot[indexToHash(2)].balance)
|
||||
assert.Equal(t, uint64(5), s.nodeByRoot[indexToHash(3)].balance)
|
||||
}
|
||||
|
||||
func TestForkChoice_IsCanonical(t *testing.T) {
|
||||
@@ -223,12 +224,12 @@ func TestForkChoice_IsCanonicalReorg(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, st, roblock))
|
||||
|
||||
f.store.emptyNodeByRoot[[32]byte{'3'}].balance = 10
|
||||
require.NoError(t, f.store.applyWeightChangesConsensusNode(ctx, f.store.treeRootNode))
|
||||
require.Equal(t, uint64(10), f.store.emptyNodeByRoot[[32]byte{'1'}].node.weight)
|
||||
require.Equal(t, uint64(0), f.store.emptyNodeByRoot[[32]byte{'2'}].node.weight)
|
||||
f.store.nodeByRoot[[32]byte{'3'}].balance = 10
|
||||
require.NoError(t, f.store.treeRootNode.applyWeightChanges(ctx))
|
||||
require.Equal(t, uint64(10), f.store.nodeByRoot[[32]byte{'1'}].weight)
|
||||
require.Equal(t, uint64(0), f.store.nodeByRoot[[32]byte{'2'}].weight)
|
||||
|
||||
require.NoError(t, f.store.updateBestDescendantConsensusNode(ctx, f.store.treeRootNode, 1, 1, 1))
|
||||
require.NoError(t, f.store.treeRootNode.updateBestDescendant(ctx, 1, 1, 1))
|
||||
require.DeepEqual(t, [32]byte{'3'}, f.store.treeRootNode.bestDescendant.root)
|
||||
|
||||
r1 := [32]byte{'1'}
|
||||
@@ -259,7 +260,7 @@ func TestForkChoice_AncestorRoot(t *testing.T) {
|
||||
st, roblock, err = prepareForkchoiceState(ctx, 5, indexToHash(3), indexToHash(2), params.BeaconConfig().ZeroHash, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, st, roblock))
|
||||
f.store.treeRootNode = f.store.emptyNodeByRoot[indexToHash(1)].node
|
||||
f.store.treeRootNode = f.store.nodeByRoot[indexToHash(1)]
|
||||
f.store.treeRootNode.parent = nil
|
||||
|
||||
r, err := f.AncestorRoot(ctx, indexToHash(3), 6)
|
||||
@@ -341,21 +342,21 @@ func TestForkChoice_RemoveEquivocating(t *testing.T) {
|
||||
|
||||
// Process b's slashing, c is now head
|
||||
f.InsertSlashedIndex(ctx, 1)
|
||||
require.Equal(t, uint64(200), f.store.emptyNodeByRoot[[32]byte{'b'}].balance)
|
||||
require.Equal(t, uint64(200), f.store.nodeByRoot[[32]byte{'b'}].balance)
|
||||
f.justifiedBalances = []uint64{100, 200, 200, 300}
|
||||
head, err = f.Head(ctx)
|
||||
require.Equal(t, uint64(200), f.store.emptyNodeByRoot[[32]byte{'b'}].weight)
|
||||
require.Equal(t, uint64(300), f.store.emptyNodeByRoot[[32]byte{'c'}].weight)
|
||||
require.Equal(t, uint64(200), f.store.nodeByRoot[[32]byte{'b'}].weight)
|
||||
require.Equal(t, uint64(300), f.store.nodeByRoot[[32]byte{'c'}].weight)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'c'}, head)
|
||||
|
||||
// Process b's slashing again, should be a noop
|
||||
f.InsertSlashedIndex(ctx, 1)
|
||||
require.Equal(t, uint64(200), f.store.emptyNodeByRoot[[32]byte{'b'}].balance)
|
||||
require.Equal(t, uint64(200), f.store.nodeByRoot[[32]byte{'b'}].balance)
|
||||
f.justifiedBalances = []uint64{100, 200, 200, 300}
|
||||
head, err = f.Head(ctx)
|
||||
require.Equal(t, uint64(200), f.store.emptyNodeByRoot[[32]byte{'b'}].weight)
|
||||
require.Equal(t, uint64(300), f.store.emptyNodeByRoot[[32]byte{'c'}].weight)
|
||||
require.Equal(t, uint64(200), f.store.nodeByRoot[[32]byte{'b'}].weight)
|
||||
require.Equal(t, uint64(300), f.store.nodeByRoot[[32]byte{'c'}].weight)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'c'}, head)
|
||||
|
||||
@@ -513,6 +514,58 @@ func TestStore_CommonAncestor(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// a -- b -- c -- d
|
||||
f = setup(0, 0)
|
||||
st, roblock, err = prepareForkchoiceState(ctx, 0, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, st, roblock))
|
||||
st, roblock, err = prepareForkchoiceState(ctx, 1, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, st, roblock))
|
||||
st, roblock, err = prepareForkchoiceState(ctx, 2, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, st, roblock))
|
||||
st, roblock, err = prepareForkchoiceState(ctx, 3, [32]byte{'d'}, [32]byte{'c'}, [32]byte{}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, st, roblock))
|
||||
tests = []struct {
|
||||
name string
|
||||
r1 [32]byte
|
||||
r2 [32]byte
|
||||
wantRoot [32]byte
|
||||
wantSlot primitives.Slot
|
||||
}{
|
||||
{
|
||||
name: "Common ancestor between a and b is a",
|
||||
r1: [32]byte{'a'},
|
||||
r2: [32]byte{'b'},
|
||||
wantRoot: [32]byte{'a'},
|
||||
wantSlot: 0,
|
||||
},
|
||||
{
|
||||
name: "Common ancestor between b and d is b",
|
||||
r1: [32]byte{'d'},
|
||||
r2: [32]byte{'b'},
|
||||
wantRoot: [32]byte{'b'},
|
||||
wantSlot: 1,
|
||||
},
|
||||
{
|
||||
name: "Common ancestor between d and a is a",
|
||||
r1: [32]byte{'d'},
|
||||
r2: [32]byte{'a'},
|
||||
wantRoot: [32]byte{'a'},
|
||||
wantSlot: 0,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
gotRoot, gotSlot, err := f.CommonAncestor(ctx, tc.r1, tc.r2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.wantRoot, gotRoot)
|
||||
require.Equal(t, tc.wantSlot, gotSlot)
|
||||
})
|
||||
}
|
||||
|
||||
// Equal inputs should return the same root.
|
||||
r, s, err := f.CommonAncestor(ctx, [32]byte{'b'}, [32]byte{'b'})
|
||||
require.NoError(t, err)
|
||||
@@ -535,9 +588,10 @@ func TestStore_CommonAncestor(t *testing.T) {
|
||||
unrealizedJustifiedEpoch: 1,
|
||||
finalizedEpoch: 1,
|
||||
unrealizedFinalizedEpoch: 1,
|
||||
optimistic: true,
|
||||
}
|
||||
|
||||
f.store.emptyNodeByRoot[[32]byte{'y'}] = &PayloadNode{node: n, optimistic: true}
|
||||
f.store.nodeByRoot[[32]byte{'y'}] = n
|
||||
// broken link
|
||||
_, _, err = f.CommonAncestor(ctx, [32]byte{'y'}, [32]byte{'a'})
|
||||
require.ErrorIs(t, err, forkchoice.ErrUnknownCommonAncestor)
|
||||
@@ -556,8 +610,7 @@ func TestStore_InsertChain(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
roblock, err := blocks.NewROBlockWithRoot(wsb, root)
|
||||
require.NoError(t, err)
|
||||
blks = append(blks, &forkchoicetypes.BlockAndCheckpoints{
|
||||
Block: roblock,
|
||||
blks = append(blks, &forkchoicetypes.BlockAndCheckpoints{Block: roblock,
|
||||
JustifiedCheckpoint: ðpb.Checkpoint{Epoch: 1, Root: params.BeaconConfig().ZeroHash[:]},
|
||||
FinalizedCheckpoint: ðpb.Checkpoint{Epoch: 1, Root: params.BeaconConfig().ZeroHash[:]},
|
||||
})
|
||||
@@ -572,8 +625,7 @@ func TestStore_InsertChain(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
roblock, err := blocks.NewROBlockWithRoot(wsb, root)
|
||||
require.NoError(t, err)
|
||||
blks = append(blks, &forkchoicetypes.BlockAndCheckpoints{
|
||||
Block: roblock,
|
||||
blks = append(blks, &forkchoicetypes.BlockAndCheckpoints{Block: roblock,
|
||||
JustifiedCheckpoint: ðpb.Checkpoint{Epoch: 1, Root: params.BeaconConfig().ZeroHash[:]},
|
||||
FinalizedCheckpoint: ðpb.Checkpoint{Epoch: 1, Root: params.BeaconConfig().ZeroHash[:]},
|
||||
})
|
||||
@@ -690,7 +742,7 @@ func TestWeight(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, st, roblock))
|
||||
|
||||
n, ok := f.store.emptyNodeByRoot[root]
|
||||
n, ok := f.store.nodeByRoot[root]
|
||||
require.Equal(t, true, ok)
|
||||
n.weight = 10
|
||||
w, err := f.Weight(root)
|
||||
@@ -862,3 +914,16 @@ func TestForkchoiceParentRoot(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, zeroHash, root)
|
||||
}
|
||||
|
||||
func TestForkChoice_CleanupInserting(t *testing.T) {
|
||||
f := setup(0, 0)
|
||||
ctx := t.Context()
|
||||
st, roblock, err := prepareForkchoiceState(ctx, 1, indexToHash(1), params.BeaconConfig().ZeroHash, params.BeaconConfig().ZeroHash, 2, 2)
|
||||
f.SetBalancesByRooter(func(_ context.Context, _ [32]byte) ([]uint64, error) {
|
||||
return f.justifiedBalances, errors.New("mock err")
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, f.InsertNode(ctx, st, roblock))
|
||||
require.Equal(t, false, f.HasNode(roblock.Root()))
|
||||
}
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
package doublylinkedtree
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/config/params"
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
|
||||
forkchoice2 "github.com/OffchainLabs/prysm/v7/consensus-types/forkchoice"
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *Store) resolveParentPayloadStatus(block interfaces.ReadOnlyBeaconBlock, parent **PayloadNode, blockHash *[32]byte) error {
|
||||
sb, err := block.Body().SignedExecutionPayloadBid()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wb, err := blocks.WrappedROSignedExecutionPayloadBid(sb)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to wrap signed bid")
|
||||
}
|
||||
bid, err := wb.Bid()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get bid from wrapped bid")
|
||||
}
|
||||
*blockHash = bid.BlockHash()
|
||||
parentRoot := block.ParentRoot()
|
||||
*parent = s.emptyNodeByRoot[parentRoot]
|
||||
if *parent == nil {
|
||||
// This is the tree root node.
|
||||
return nil
|
||||
}
|
||||
if bid.ParentBlockHash() == (*parent).node.blockHash {
|
||||
// block builds on full
|
||||
*parent = s.fullNodeByRoot[(*parent).node.root]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyWeightChangesConsensusNode recomputes the weight of the node passed as an argument and all of its descendants,
|
||||
// using the current balance stored in each node.
|
||||
func (s *Store) applyWeightChangesConsensusNode(ctx context.Context, n *Node) error {
|
||||
// Recursively calling the children to sum their weights.
|
||||
en := s.emptyNodeByRoot[n.root]
|
||||
if err := s.applyWeightChangesPayloadNode(ctx, en); err != nil {
|
||||
return err
|
||||
}
|
||||
childrenWeight := en.weight
|
||||
fn := s.fullNodeByRoot[n.root]
|
||||
if fn != nil {
|
||||
if err := s.applyWeightChangesPayloadNode(ctx, fn); err != nil {
|
||||
return err
|
||||
}
|
||||
childrenWeight += fn.weight
|
||||
}
|
||||
if n.root == params.BeaconConfig().ZeroHash {
|
||||
return nil
|
||||
}
|
||||
n.weight = n.balance + childrenWeight
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyWeightChangesPayloadNode recomputes the weight of the node passed as an argument and all of its descendants,
|
||||
// using the current balance stored in each node.
|
||||
func (s *Store) applyWeightChangesPayloadNode(ctx context.Context, n *PayloadNode) error {
|
||||
// Recursively calling the children to sum their weights.
|
||||
childrenWeight := uint64(0)
|
||||
for _, child := range n.children {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
if err := s.applyWeightChangesConsensusNode(ctx, child); err != nil {
|
||||
return err
|
||||
}
|
||||
childrenWeight += child.weight
|
||||
}
|
||||
n.weight = n.balance + childrenWeight
|
||||
return nil
|
||||
}
|
||||
|
||||
// allConsensusChildren returns the list of all consensus blocks that build on the given node.
|
||||
func (s *Store) allConsensusChildren(n *Node) []*Node {
|
||||
en := s.emptyNodeByRoot[n.root]
|
||||
fn, ok := s.fullNodeByRoot[n.root]
|
||||
if ok {
|
||||
return append(slices.Clone(en.children), fn.children...)
|
||||
}
|
||||
return en.children
|
||||
}
|
||||
|
||||
// setNodeAndParentValidated sets the current node and all the ancestors as validated (i.e. non-optimistic).
|
||||
func (s *Store) setNodeAndParentValidated(ctx context.Context, pn *PayloadNode) error {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
if !pn.optimistic {
|
||||
return nil
|
||||
}
|
||||
pn.optimistic = false
|
||||
if pn.full {
|
||||
// set the empty node also a as valid
|
||||
en := s.emptyNodeByRoot[pn.node.root]
|
||||
en.optimistic = false
|
||||
}
|
||||
if pn.node.parent == nil {
|
||||
return nil
|
||||
}
|
||||
return s.setNodeAndParentValidated(ctx, pn.node.parent)
|
||||
}
|
||||
|
||||
// fullParent returns the latest full node that this block builds on.
|
||||
func (s *Store) fullParent(pn *PayloadNode) *PayloadNode {
|
||||
parent := pn.node.parent
|
||||
for ; parent != nil && !parent.full; parent = parent.node.parent {
|
||||
}
|
||||
return parent
|
||||
}
|
||||
|
||||
// parentHash return the payload hash of the latest full node that this block builds on.
|
||||
func (s *Store) parentHash(pn *PayloadNode) [32]byte {
|
||||
fullParent := s.fullParent(pn)
|
||||
if fullParent == nil {
|
||||
return [32]byte{}
|
||||
}
|
||||
return fullParent.node.blockHash
|
||||
}
|
||||
|
||||
// latestHashForRoot returns the latest payload hash for the given block root.
|
||||
func (s *Store) latestHashForRoot(root [32]byte) [32]byte {
|
||||
// try to get the full node first
|
||||
fn := s.fullNodeByRoot[root]
|
||||
if fn != nil {
|
||||
return fn.node.blockHash
|
||||
}
|
||||
en := s.emptyNodeByRoot[root]
|
||||
if en == nil {
|
||||
// This should not happen
|
||||
return [32]byte{}
|
||||
}
|
||||
return s.parentHash(en)
|
||||
}
|
||||
|
||||
// updateBestDescendantPayloadNode updates the best descendant of this node and its
|
||||
// children.
|
||||
func (s *Store) updateBestDescendantPayloadNode(ctx context.Context, n *PayloadNode, justifiedEpoch, finalizedEpoch, currentEpoch primitives.Epoch) error {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
var bestChild *Node
|
||||
bestWeight := uint64(0)
|
||||
for _, child := range n.children {
|
||||
if child == nil {
|
||||
return errors.Wrap(ErrNilNode, "could not update best descendant")
|
||||
}
|
||||
if err := s.updateBestDescendantConsensusNode(ctx, child, justifiedEpoch, finalizedEpoch, currentEpoch); err != nil {
|
||||
return err
|
||||
}
|
||||
childLeadsToViableHead := child.leadsToViableHead(justifiedEpoch, currentEpoch)
|
||||
if childLeadsToViableHead && bestChild == nil {
|
||||
// The child leads to a viable head, but the current
|
||||
// parent's best child doesn't.
|
||||
bestWeight = child.weight
|
||||
bestChild = child
|
||||
} else if childLeadsToViableHead {
|
||||
// If both are viable, compare their weights.
|
||||
if child.weight == bestWeight {
|
||||
// Tie-breaker of equal weights by root.
|
||||
if bytes.Compare(child.root[:], bestChild.root[:]) > 0 {
|
||||
bestChild = child
|
||||
}
|
||||
} else if child.weight > bestWeight {
|
||||
bestChild = child
|
||||
bestWeight = child.weight
|
||||
}
|
||||
}
|
||||
}
|
||||
if bestChild == nil {
|
||||
n.bestDescendant = nil
|
||||
} else {
|
||||
if bestChild.bestDescendant == nil {
|
||||
n.bestDescendant = bestChild
|
||||
} else {
|
||||
n.bestDescendant = bestChild.bestDescendant
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateBestDescendantConsensusNode updates the best descendant of this node and its
|
||||
// children.
|
||||
func (s *Store) updateBestDescendantConsensusNode(ctx context.Context, n *Node, justifiedEpoch, finalizedEpoch, currentEpoch primitives.Epoch) error {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
if len(s.allConsensusChildren(n)) == 0 {
|
||||
n.bestDescendant = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
en := s.emptyNodeByRoot[n.root]
|
||||
if err := s.updateBestDescendantPayloadNode(ctx, en, justifiedEpoch, finalizedEpoch, currentEpoch); err != nil {
|
||||
return err
|
||||
}
|
||||
fn := s.fullNodeByRoot[n.root]
|
||||
if fn == nil {
|
||||
n.bestDescendant = en.bestDescendant
|
||||
return nil
|
||||
}
|
||||
// TODO GLOAS: pick between full or empty
|
||||
if err := s.updateBestDescendantPayloadNode(ctx, fn, justifiedEpoch, finalizedEpoch, currentEpoch); err != nil {
|
||||
return err
|
||||
}
|
||||
n.bestDescendant = fn.bestDescendant
|
||||
return nil
|
||||
}
|
||||
|
||||
// choosePayloadContent chooses between empty or full for the passed consensus node. TODO Gloas: use PTC to choose.
|
||||
func (s *Store) choosePayloadContent(n *Node) *PayloadNode {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
fn := s.fullNodeByRoot[n.root]
|
||||
if fn != nil {
|
||||
return fn
|
||||
}
|
||||
return s.emptyNodeByRoot[n.root]
|
||||
}
|
||||
|
||||
// nodeTreeDump appends to the given list all the nodes descending from this one
|
||||
func (s *Store) nodeTreeDump(ctx context.Context, n *Node, nodes []*forkchoice2.Node) ([]*forkchoice2.Node, error) {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
var parentRoot [32]byte
|
||||
if n.parent != nil {
|
||||
parentRoot = n.parent.node.root
|
||||
}
|
||||
target := [32]byte{}
|
||||
if n.target != nil {
|
||||
target = n.target.root
|
||||
}
|
||||
optimistic := false
|
||||
if n.parent != nil {
|
||||
optimistic = n.parent.optimistic
|
||||
}
|
||||
en := s.emptyNodeByRoot[n.root]
|
||||
timestamp := en.timestamp
|
||||
fn := s.fullNodeByRoot[n.root]
|
||||
if fn != nil {
|
||||
optimistic = fn.optimistic
|
||||
timestamp = fn.timestamp
|
||||
}
|
||||
thisNode := &forkchoice2.Node{
|
||||
Slot: n.slot,
|
||||
BlockRoot: n.root[:],
|
||||
ParentRoot: parentRoot[:],
|
||||
JustifiedEpoch: n.justifiedEpoch,
|
||||
FinalizedEpoch: n.finalizedEpoch,
|
||||
UnrealizedJustifiedEpoch: n.unrealizedJustifiedEpoch,
|
||||
UnrealizedFinalizedEpoch: n.unrealizedFinalizedEpoch,
|
||||
Balance: n.balance,
|
||||
Weight: n.weight,
|
||||
ExecutionOptimistic: optimistic,
|
||||
ExecutionBlockHash: n.blockHash[:],
|
||||
Timestamp: timestamp,
|
||||
Target: target[:],
|
||||
}
|
||||
if optimistic {
|
||||
thisNode.Validity = forkchoice2.Optimistic
|
||||
} else {
|
||||
thisNode.Validity = forkchoice2.Valid
|
||||
}
|
||||
|
||||
nodes = append(nodes, thisNode)
|
||||
var err error
|
||||
children := s.allConsensusChildren(n)
|
||||
for _, child := range children {
|
||||
nodes, err = s.nodeTreeDump(ctx, child, nodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nodes, nil
|
||||
}
|
||||
26
beacon-chain/forkchoice/doubly-linked-tree/last_root.go
Normal file
26
beacon-chain/forkchoice/doubly-linked-tree/last_root.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package doublylinkedtree
|
||||
|
||||
import (
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
||||
"github.com/OffchainLabs/prysm/v7/time/slots"
|
||||
)
|
||||
|
||||
// LastRoot returns the last canonical block root in the given epoch
|
||||
func (f *ForkChoice) LastRoot(epoch primitives.Epoch) [32]byte {
|
||||
head := f.store.headNode
|
||||
headEpoch := slots.ToEpoch(head.slot)
|
||||
epochEnd, err := slots.EpochEnd(epoch)
|
||||
if err != nil {
|
||||
return [32]byte{}
|
||||
}
|
||||
if headEpoch <= epoch {
|
||||
return head.root
|
||||
}
|
||||
for head != nil && head.slot > epochEnd {
|
||||
head = head.parent
|
||||
}
|
||||
if head == nil {
|
||||
return [32]byte{}
|
||||
}
|
||||
return head.root
|
||||
}
|
||||
38
beacon-chain/forkchoice/doubly-linked-tree/last_root_test.go
Normal file
38
beacon-chain/forkchoice/doubly-linked-tree/last_root_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package doublylinkedtree
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/config/params"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/require"
|
||||
)
|
||||
|
||||
func TestLastRoot(t *testing.T) {
|
||||
f := setup(0, 0)
|
||||
ctx := t.Context()
|
||||
|
||||
st, root, err := prepareForkchoiceState(ctx, 1, [32]byte{'1'}, params.BeaconConfig().ZeroHash, [32]byte{'1'}, 0, 0)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, st, root))
|
||||
st, root, err = prepareForkchoiceState(ctx, 2, [32]byte{'2'}, [32]byte{'1'}, [32]byte{'2'}, 0, 0)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, st, root))
|
||||
st, root, err = prepareForkchoiceState(ctx, 3, [32]byte{'3'}, [32]byte{'1'}, [32]byte{'3'}, 0, 0)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, st, root))
|
||||
st, root, err = prepareForkchoiceState(ctx, 32, [32]byte{'4'}, [32]byte{'3'}, [32]byte{'4'}, 0, 0)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, st, root))
|
||||
st, root, err = prepareForkchoiceState(ctx, 33, [32]byte{'5'}, [32]byte{'2'}, [32]byte{'5'}, 0, 0)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, st, root))
|
||||
st, root, err = prepareForkchoiceState(ctx, 34, [32]byte{'6'}, [32]byte{'5'}, [32]byte{'6'}, 0, 0)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, st, root))
|
||||
headNode := f.store.nodeByRoot[[32]byte{'6'}]
|
||||
f.store.headNode = headNode
|
||||
require.Equal(t, [32]byte{'6'}, f.store.headNode.root)
|
||||
require.Equal(t, [32]byte{'2'}, f.LastRoot(0))
|
||||
require.Equal(t, [32]byte{'6'}, f.LastRoot(1))
|
||||
require.Equal(t, [32]byte{'6'}, f.LastRoot(2))
|
||||
}
|
||||
@@ -1,17 +1,95 @@
|
||||
package doublylinkedtree
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/config/params"
|
||||
forkchoice2 "github.com/OffchainLabs/prysm/v7/consensus-types/forkchoice"
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
||||
"github.com/OffchainLabs/prysm/v7/time/slots"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ProcessAttestationsThreshold is the amount of time after which we
|
||||
// process attestations for the current slot
|
||||
const ProcessAttestationsThreshold = 10 * time.Second
|
||||
|
||||
// applyWeightChanges recomputes the weight of the node passed as an argument and all of its descendants,
|
||||
// using the current balance stored in each node.
|
||||
func (n *Node) applyWeightChanges(ctx context.Context) error {
|
||||
// Recursively calling the children to sum their weights.
|
||||
childrenWeight := uint64(0)
|
||||
for _, child := range n.children {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
if err := child.applyWeightChanges(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
childrenWeight += child.weight
|
||||
}
|
||||
if n.root == params.BeaconConfig().ZeroHash {
|
||||
return nil
|
||||
}
|
||||
n.weight = n.balance + childrenWeight
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateBestDescendant updates the best descendant of this node and its
|
||||
// children.
|
||||
func (n *Node) updateBestDescendant(ctx context.Context, justifiedEpoch, finalizedEpoch, currentEpoch primitives.Epoch) error {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
if len(n.children) == 0 {
|
||||
n.bestDescendant = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
var bestChild *Node
|
||||
bestWeight := uint64(0)
|
||||
hasViableDescendant := false
|
||||
for _, child := range n.children {
|
||||
if child == nil {
|
||||
return errors.Wrap(ErrNilNode, "could not update best descendant")
|
||||
}
|
||||
if err := child.updateBestDescendant(ctx, justifiedEpoch, finalizedEpoch, currentEpoch); err != nil {
|
||||
return err
|
||||
}
|
||||
childLeadsToViableHead := child.leadsToViableHead(justifiedEpoch, currentEpoch)
|
||||
if childLeadsToViableHead && !hasViableDescendant {
|
||||
// The child leads to a viable head, but the current
|
||||
// parent's best child doesn't.
|
||||
bestWeight = child.weight
|
||||
bestChild = child
|
||||
hasViableDescendant = true
|
||||
} else if childLeadsToViableHead {
|
||||
// If both are viable, compare their weights.
|
||||
if child.weight == bestWeight {
|
||||
// Tie-breaker of equal weights by root.
|
||||
if bytes.Compare(child.root[:], bestChild.root[:]) > 0 {
|
||||
bestChild = child
|
||||
}
|
||||
} else if child.weight > bestWeight {
|
||||
bestChild = child
|
||||
bestWeight = child.weight
|
||||
}
|
||||
}
|
||||
}
|
||||
if hasViableDescendant {
|
||||
if bestChild.bestDescendant == nil {
|
||||
n.bestDescendant = bestChild
|
||||
} else {
|
||||
n.bestDescendant = bestChild.bestDescendant
|
||||
}
|
||||
} else {
|
||||
n.bestDescendant = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// viableForHead returns true if the node is viable to head.
|
||||
// Any node with different finalized or justified epoch than
|
||||
// the ones in fork choice store should not be viable to head.
|
||||
@@ -32,13 +110,30 @@ func (n *Node) leadsToViableHead(justifiedEpoch, currentEpoch primitives.Epoch)
|
||||
return n.bestDescendant.viableForHead(justifiedEpoch, currentEpoch)
|
||||
}
|
||||
|
||||
// setNodeAndParentValidated sets the current node and all the ancestors as validated (i.e. non-optimistic).
|
||||
func (n *Node) setNodeAndParentValidated(ctx context.Context) error {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
if !n.optimistic {
|
||||
return nil
|
||||
}
|
||||
n.optimistic = false
|
||||
|
||||
if n.parent == nil {
|
||||
return nil
|
||||
}
|
||||
return n.parent.setNodeAndParentValidated(ctx)
|
||||
}
|
||||
|
||||
// arrivedEarly returns whether this node was inserted before the first
|
||||
// threshold to orphan a block.
|
||||
// Note that genesisTime has seconds granularity, therefore we use a strict
|
||||
// inequality < here. For example a block that arrives 3.9999 seconds into the
|
||||
// slot will have secs = 3 below.
|
||||
func (n *PayloadNode) arrivedEarly(genesis time.Time) (bool, error) {
|
||||
sss, err := slots.SinceSlotStart(n.node.slot, genesis, n.timestamp.Truncate(time.Second)) // Truncate such that 3.9999 seconds will have a value of 3.
|
||||
func (n *Node) arrivedEarly(genesis time.Time) (bool, error) {
|
||||
sss, err := slots.SinceSlotStart(n.slot, genesis, n.timestamp.Truncate(time.Second)) // Truncate such that 3.9999 seconds will have a value of 3.
|
||||
votingWindow := params.BeaconConfig().SlotComponentDuration(params.BeaconConfig().AttestationDueBPS)
|
||||
return sss < votingWindow, err
|
||||
}
|
||||
@@ -48,7 +143,52 @@ func (n *PayloadNode) arrivedEarly(genesis time.Time) (bool, error) {
|
||||
// Note that genesisTime has seconds granularity, therefore we use an
|
||||
// inequality >= here. For example a block that arrives 10.00001 seconds into the
|
||||
// slot will have secs = 10 below.
|
||||
func (n *PayloadNode) arrivedAfterOrphanCheck(genesis time.Time) (bool, error) {
|
||||
secs, err := slots.SinceSlotStart(n.node.slot, genesis, n.timestamp.Truncate(time.Second)) // Truncate such that 10.00001 seconds will have a value of 10.
|
||||
func (n *Node) arrivedAfterOrphanCheck(genesis time.Time) (bool, error) {
|
||||
secs, err := slots.SinceSlotStart(n.slot, genesis, n.timestamp.Truncate(time.Second)) // Truncate such that 10.00001 seconds will have a value of 10.
|
||||
return secs >= ProcessAttestationsThreshold, err
|
||||
}
|
||||
|
||||
// nodeTreeDump appends to the given list all the nodes descending from this one
|
||||
func (n *Node) nodeTreeDump(ctx context.Context, nodes []*forkchoice2.Node) ([]*forkchoice2.Node, error) {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
var parentRoot [32]byte
|
||||
if n.parent != nil {
|
||||
parentRoot = n.parent.root
|
||||
}
|
||||
target := [32]byte{}
|
||||
if n.target != nil {
|
||||
target = n.target.root
|
||||
}
|
||||
thisNode := &forkchoice2.Node{
|
||||
Slot: n.slot,
|
||||
BlockRoot: n.root[:],
|
||||
ParentRoot: parentRoot[:],
|
||||
JustifiedEpoch: n.justifiedEpoch,
|
||||
FinalizedEpoch: n.finalizedEpoch,
|
||||
UnrealizedJustifiedEpoch: n.unrealizedJustifiedEpoch,
|
||||
UnrealizedFinalizedEpoch: n.unrealizedFinalizedEpoch,
|
||||
Balance: n.balance,
|
||||
Weight: n.weight,
|
||||
ExecutionOptimistic: n.optimistic,
|
||||
ExecutionBlockHash: n.payloadHash[:],
|
||||
Timestamp: n.timestamp,
|
||||
Target: target[:],
|
||||
}
|
||||
if n.optimistic {
|
||||
thisNode.Validity = forkchoice2.Optimistic
|
||||
} else {
|
||||
thisNode.Validity = forkchoice2.Valid
|
||||
}
|
||||
|
||||
nodes = append(nodes, thisNode)
|
||||
var err error
|
||||
for _, child := range n.children {
|
||||
nodes, err = child.nodeTreeDump(ctx, nodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
@@ -27,15 +27,15 @@ func TestNode_ApplyWeightChanges_PositiveChange(t *testing.T) {
|
||||
// The updated balances of each node is 100
|
||||
s := f.store
|
||||
|
||||
s.emptyNodeByRoot[indexToHash(1)].balance = 100
|
||||
s.emptyNodeByRoot[indexToHash(2)].balance = 100
|
||||
s.emptyNodeByRoot[indexToHash(3)].balance = 100
|
||||
s.nodeByRoot[indexToHash(1)].balance = 100
|
||||
s.nodeByRoot[indexToHash(2)].balance = 100
|
||||
s.nodeByRoot[indexToHash(3)].balance = 100
|
||||
|
||||
assert.NoError(t, s.applyWeightChangesConsensusNode(ctx, s.treeRootNode))
|
||||
assert.NoError(t, s.treeRootNode.applyWeightChanges(ctx))
|
||||
|
||||
assert.Equal(t, uint64(300), s.emptyNodeByRoot[indexToHash(1)].node.weight)
|
||||
assert.Equal(t, uint64(200), s.emptyNodeByRoot[indexToHash(2)].node.weight)
|
||||
assert.Equal(t, uint64(100), s.emptyNodeByRoot[indexToHash(3)].node.weight)
|
||||
assert.Equal(t, uint64(300), s.nodeByRoot[indexToHash(1)].weight)
|
||||
assert.Equal(t, uint64(200), s.nodeByRoot[indexToHash(2)].weight)
|
||||
assert.Equal(t, uint64(100), s.nodeByRoot[indexToHash(3)].weight)
|
||||
}
|
||||
|
||||
func TestNode_ApplyWeightChanges_NegativeChange(t *testing.T) {
|
||||
@@ -53,19 +53,19 @@ func TestNode_ApplyWeightChanges_NegativeChange(t *testing.T) {
|
||||
|
||||
// The updated balances of each node is 100
|
||||
s := f.store
|
||||
s.emptyNodeByRoot[indexToHash(1)].weight = 400
|
||||
s.emptyNodeByRoot[indexToHash(2)].weight = 400
|
||||
s.emptyNodeByRoot[indexToHash(3)].weight = 400
|
||||
s.nodeByRoot[indexToHash(1)].weight = 400
|
||||
s.nodeByRoot[indexToHash(2)].weight = 400
|
||||
s.nodeByRoot[indexToHash(3)].weight = 400
|
||||
|
||||
s.emptyNodeByRoot[indexToHash(1)].balance = 100
|
||||
s.emptyNodeByRoot[indexToHash(2)].balance = 100
|
||||
s.emptyNodeByRoot[indexToHash(3)].balance = 100
|
||||
s.nodeByRoot[indexToHash(1)].balance = 100
|
||||
s.nodeByRoot[indexToHash(2)].balance = 100
|
||||
s.nodeByRoot[indexToHash(3)].balance = 100
|
||||
|
||||
assert.NoError(t, s.applyWeightChangesConsensusNode(ctx, s.treeRootNode))
|
||||
assert.NoError(t, s.treeRootNode.applyWeightChanges(ctx))
|
||||
|
||||
assert.Equal(t, uint64(300), s.emptyNodeByRoot[indexToHash(1)].node.weight)
|
||||
assert.Equal(t, uint64(200), s.emptyNodeByRoot[indexToHash(2)].node.weight)
|
||||
assert.Equal(t, uint64(100), s.emptyNodeByRoot[indexToHash(3)].node.weight)
|
||||
assert.Equal(t, uint64(300), s.nodeByRoot[indexToHash(1)].weight)
|
||||
assert.Equal(t, uint64(200), s.nodeByRoot[indexToHash(2)].weight)
|
||||
assert.Equal(t, uint64(100), s.nodeByRoot[indexToHash(3)].weight)
|
||||
}
|
||||
|
||||
func TestNode_UpdateBestDescendant_NonViableChild(t *testing.T) {
|
||||
@@ -78,7 +78,7 @@ func TestNode_UpdateBestDescendant_NonViableChild(t *testing.T) {
|
||||
|
||||
// Verify parent's best child and best descendant are `none`.
|
||||
s := f.store
|
||||
assert.Equal(t, 1, len(s.allConsensusChildren(s.treeRootNode)))
|
||||
assert.Equal(t, 1, len(s.treeRootNode.children))
|
||||
nilBestDescendant := s.treeRootNode.bestDescendant == nil
|
||||
assert.Equal(t, true, nilBestDescendant)
|
||||
}
|
||||
@@ -92,9 +92,8 @@ func TestNode_UpdateBestDescendant_ViableChild(t *testing.T) {
|
||||
require.NoError(t, f.InsertNode(ctx, state, blk))
|
||||
|
||||
s := f.store
|
||||
children := s.allConsensusChildren(s.treeRootNode)
|
||||
assert.Equal(t, 1, len(children))
|
||||
assert.Equal(t, children[0], s.treeRootNode.bestDescendant)
|
||||
assert.Equal(t, 1, len(s.treeRootNode.children))
|
||||
assert.Equal(t, s.treeRootNode.children[0], s.treeRootNode.bestDescendant)
|
||||
}
|
||||
|
||||
func TestNode_UpdateBestDescendant_HigherWeightChild(t *testing.T) {
|
||||
@@ -109,34 +108,32 @@ func TestNode_UpdateBestDescendant_HigherWeightChild(t *testing.T) {
|
||||
require.NoError(t, f.InsertNode(ctx, state, blk))
|
||||
|
||||
s := f.store
|
||||
s.emptyNodeByRoot[indexToHash(1)].weight = 100
|
||||
s.emptyNodeByRoot[indexToHash(2)].weight = 200
|
||||
assert.NoError(t, s.updateBestDescendantConsensusNode(ctx, s.treeRootNode, 1, 1, 1))
|
||||
s.nodeByRoot[indexToHash(1)].weight = 100
|
||||
s.nodeByRoot[indexToHash(2)].weight = 200
|
||||
assert.NoError(t, s.treeRootNode.updateBestDescendant(ctx, 1, 1, 1))
|
||||
|
||||
children := s.allConsensusChildren(s.treeRootNode)
|
||||
assert.Equal(t, 2, len(children))
|
||||
assert.Equal(t, children[1], s.treeRootNode.bestDescendant)
|
||||
assert.Equal(t, 2, len(s.treeRootNode.children))
|
||||
assert.Equal(t, s.treeRootNode.children[1], s.treeRootNode.bestDescendant)
|
||||
}
|
||||
|
||||
func TestNode_UpdateBestDescendant_LowerWeightChild(t *testing.T) {
|
||||
f := setup(1, 1)
|
||||
ctx := t.Context()
|
||||
// Input child is the best descendant
|
||||
state, blk, err := prepareForkchoiceState(ctx, 1, indexToHash(1), params.BeaconConfig().ZeroHash, indexToHash(101), 1, 1)
|
||||
state, blk, err := prepareForkchoiceState(ctx, 1, indexToHash(1), params.BeaconConfig().ZeroHash, params.BeaconConfig().ZeroHash, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blk))
|
||||
state, blk, err = prepareForkchoiceState(ctx, 2, indexToHash(2), params.BeaconConfig().ZeroHash, indexToHash(102), 1, 1)
|
||||
state, blk, err = prepareForkchoiceState(ctx, 2, indexToHash(2), params.BeaconConfig().ZeroHash, params.BeaconConfig().ZeroHash, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blk))
|
||||
|
||||
s := f.store
|
||||
s.emptyNodeByRoot[indexToHash(1)].node.weight = 200
|
||||
s.emptyNodeByRoot[indexToHash(2)].node.weight = 100
|
||||
assert.NoError(t, s.updateBestDescendantConsensusNode(ctx, s.treeRootNode, 1, 1, 1))
|
||||
s.nodeByRoot[indexToHash(1)].weight = 200
|
||||
s.nodeByRoot[indexToHash(2)].weight = 100
|
||||
assert.NoError(t, s.treeRootNode.updateBestDescendant(ctx, 1, 1, 1))
|
||||
|
||||
children := s.allConsensusChildren(s.treeRootNode)
|
||||
assert.Equal(t, 2, len(children))
|
||||
assert.Equal(t, children[0], s.treeRootNode.bestDescendant)
|
||||
assert.Equal(t, 2, len(s.treeRootNode.children))
|
||||
assert.Equal(t, s.treeRootNode.children[0], s.treeRootNode.bestDescendant)
|
||||
}
|
||||
|
||||
func TestNode_ViableForHead(t *testing.T) {
|
||||
@@ -179,44 +176,44 @@ func TestNode_LeadsToViableHead(t *testing.T) {
|
||||
require.NoError(t, f.InsertNode(ctx, state, blk))
|
||||
|
||||
require.Equal(t, true, f.store.treeRootNode.leadsToViableHead(4, 5))
|
||||
require.Equal(t, true, f.store.emptyNodeByRoot[indexToHash(5)].node.leadsToViableHead(4, 5))
|
||||
require.Equal(t, false, f.store.emptyNodeByRoot[indexToHash(2)].node.leadsToViableHead(4, 5))
|
||||
require.Equal(t, false, f.store.emptyNodeByRoot[indexToHash(4)].node.leadsToViableHead(4, 5))
|
||||
require.Equal(t, true, f.store.nodeByRoot[indexToHash(5)].leadsToViableHead(4, 5))
|
||||
require.Equal(t, false, f.store.nodeByRoot[indexToHash(2)].leadsToViableHead(4, 5))
|
||||
require.Equal(t, false, f.store.nodeByRoot[indexToHash(4)].leadsToViableHead(4, 5))
|
||||
}
|
||||
|
||||
func TestNode_SetFullyValidated(t *testing.T) {
|
||||
f := setup(1, 1)
|
||||
ctx := t.Context()
|
||||
storeNodes := make([]*PayloadNode, 6)
|
||||
storeNodes[0] = f.store.fullNodeByRoot[params.BeaconConfig().ZeroHash]
|
||||
storeNodes := make([]*Node, 6)
|
||||
storeNodes[0] = f.store.treeRootNode
|
||||
// insert blocks in the fork pattern (optimistic status in parenthesis)
|
||||
//
|
||||
// 0 (false) -- 1 (false) -- 2 (false) -- 3 (true) -- 4 (true)
|
||||
// \
|
||||
// -- 5 (true)
|
||||
//
|
||||
state, blk, err := prepareForkchoiceState(ctx, 1, indexToHash(1), params.BeaconConfig().ZeroHash, indexToHash(101), 1, 1)
|
||||
state, blk, err := prepareForkchoiceState(ctx, 1, indexToHash(1), params.BeaconConfig().ZeroHash, params.BeaconConfig().ZeroHash, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blk))
|
||||
storeNodes[1] = f.store.fullNodeByRoot[blk.Root()]
|
||||
storeNodes[1] = f.store.nodeByRoot[blk.Root()]
|
||||
require.NoError(t, f.SetOptimisticToValid(ctx, params.BeaconConfig().ZeroHash))
|
||||
state, blk, err = prepareForkchoiceState(ctx, 2, indexToHash(2), indexToHash(1), params.BeaconConfig().ZeroHash, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blk))
|
||||
storeNodes[2] = f.store.nodeByRoot[blk.Root()]
|
||||
require.NoError(t, f.SetOptimisticToValid(ctx, indexToHash(1)))
|
||||
state, blk, err = prepareForkchoiceState(ctx, 2, indexToHash(2), indexToHash(1), indexToHash(102), 1, 1)
|
||||
state, blk, err = prepareForkchoiceState(ctx, 3, indexToHash(3), indexToHash(2), params.BeaconConfig().ZeroHash, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blk))
|
||||
storeNodes[2] = f.store.fullNodeByRoot[blk.Root()]
|
||||
require.NoError(t, f.SetOptimisticToValid(ctx, indexToHash(2)))
|
||||
state, blk, err = prepareForkchoiceState(ctx, 3, indexToHash(3), indexToHash(2), indexToHash(103), 1, 1)
|
||||
storeNodes[3] = f.store.nodeByRoot[blk.Root()]
|
||||
state, blk, err = prepareForkchoiceState(ctx, 4, indexToHash(4), indexToHash(3), params.BeaconConfig().ZeroHash, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blk))
|
||||
storeNodes[3] = f.store.fullNodeByRoot[blk.Root()]
|
||||
state, blk, err = prepareForkchoiceState(ctx, 4, indexToHash(4), indexToHash(3), indexToHash(104), 1, 1)
|
||||
storeNodes[4] = f.store.nodeByRoot[blk.Root()]
|
||||
state, blk, err = prepareForkchoiceState(ctx, 5, indexToHash(5), indexToHash(1), params.BeaconConfig().ZeroHash, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blk))
|
||||
storeNodes[4] = f.store.fullNodeByRoot[blk.Root()]
|
||||
state, blk, err = prepareForkchoiceState(ctx, 5, indexToHash(5), indexToHash(1), indexToHash(105), 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blk))
|
||||
storeNodes[5] = f.store.fullNodeByRoot[blk.Root()]
|
||||
storeNodes[5] = f.store.nodeByRoot[blk.Root()]
|
||||
|
||||
opt, err := f.IsOptimistic(indexToHash(5))
|
||||
require.NoError(t, err)
|
||||
@@ -226,7 +223,7 @@ func TestNode_SetFullyValidated(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, true, opt)
|
||||
|
||||
require.NoError(t, f.store.setNodeAndParentValidated(ctx, f.store.fullNodeByRoot[indexToHash(4)]))
|
||||
require.NoError(t, f.store.nodeByRoot[indexToHash(4)].setNodeAndParentValidated(ctx))
|
||||
|
||||
// block 5 should still be optimistic
|
||||
opt, err = f.IsOptimistic(indexToHash(5))
|
||||
@@ -243,20 +240,20 @@ func TestNode_SetFullyValidated(t *testing.T) {
|
||||
require.Equal(t, false, opt)
|
||||
|
||||
respNodes := make([]*forkchoice.Node, 0)
|
||||
respNodes, err = f.store.nodeTreeDump(ctx, f.store.treeRootNode, respNodes)
|
||||
respNodes, err = f.store.treeRootNode.nodeTreeDump(ctx, respNodes)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(respNodes), f.NodeCount())
|
||||
|
||||
for i, respNode := range respNodes {
|
||||
require.Equal(t, storeNodes[i].node.slot, respNode.Slot)
|
||||
require.DeepEqual(t, storeNodes[i].node.root[:], respNode.BlockRoot)
|
||||
require.Equal(t, storeNodes[i].node.balance, respNode.Balance)
|
||||
require.Equal(t, storeNodes[i].node.weight, respNode.Weight)
|
||||
require.Equal(t, storeNodes[i].slot, respNode.Slot)
|
||||
require.DeepEqual(t, storeNodes[i].root[:], respNode.BlockRoot)
|
||||
require.Equal(t, storeNodes[i].balance, respNode.Balance)
|
||||
require.Equal(t, storeNodes[i].weight, respNode.Weight)
|
||||
require.Equal(t, storeNodes[i].optimistic, respNode.ExecutionOptimistic)
|
||||
require.Equal(t, storeNodes[i].node.justifiedEpoch, respNode.JustifiedEpoch)
|
||||
require.Equal(t, storeNodes[i].node.unrealizedJustifiedEpoch, respNode.UnrealizedJustifiedEpoch)
|
||||
require.Equal(t, storeNodes[i].node.finalizedEpoch, respNode.FinalizedEpoch)
|
||||
require.Equal(t, storeNodes[i].node.unrealizedFinalizedEpoch, respNode.UnrealizedFinalizedEpoch)
|
||||
require.Equal(t, storeNodes[i].justifiedEpoch, respNode.JustifiedEpoch)
|
||||
require.Equal(t, storeNodes[i].unrealizedJustifiedEpoch, respNode.UnrealizedJustifiedEpoch)
|
||||
require.Equal(t, storeNodes[i].finalizedEpoch, respNode.FinalizedEpoch)
|
||||
require.Equal(t, storeNodes[i].unrealizedFinalizedEpoch, respNode.UnrealizedFinalizedEpoch)
|
||||
require.Equal(t, storeNodes[i].timestamp, respNode.Timestamp)
|
||||
}
|
||||
}
|
||||
@@ -275,10 +272,10 @@ func TestNode_TimeStampsChecks(t *testing.T) {
|
||||
headRoot, err := f.Head(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, root, headRoot)
|
||||
early, err := f.store.choosePayloadContent(f.store.headNode).arrivedEarly(f.store.genesisTime)
|
||||
early, err := f.store.headNode.arrivedEarly(f.store.genesisTime)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, true, early)
|
||||
late, err := f.store.choosePayloadContent(f.store.headNode).arrivedAfterOrphanCheck(f.store.genesisTime)
|
||||
late, err := f.store.headNode.arrivedAfterOrphanCheck(f.store.genesisTime)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, false, late)
|
||||
|
||||
@@ -292,10 +289,10 @@ func TestNode_TimeStampsChecks(t *testing.T) {
|
||||
headRoot, err = f.Head(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, root, headRoot)
|
||||
early, err = f.store.choosePayloadContent(f.store.headNode).arrivedEarly(f.store.genesisTime)
|
||||
early, err = f.store.headNode.arrivedEarly(f.store.genesisTime)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, false, early)
|
||||
late, err = f.store.choosePayloadContent(f.store.headNode).arrivedAfterOrphanCheck(f.store.genesisTime)
|
||||
late, err = f.store.headNode.arrivedAfterOrphanCheck(f.store.genesisTime)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, false, late)
|
||||
|
||||
@@ -308,10 +305,10 @@ func TestNode_TimeStampsChecks(t *testing.T) {
|
||||
headRoot, err = f.Head(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, root, headRoot)
|
||||
early, err = f.store.choosePayloadContent(f.store.headNode).arrivedEarly(f.store.genesisTime)
|
||||
early, err = f.store.headNode.arrivedEarly(f.store.genesisTime)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, false, early)
|
||||
late, err = f.store.choosePayloadContent(f.store.headNode).arrivedAfterOrphanCheck(f.store.genesisTime)
|
||||
late, err = f.store.headNode.arrivedAfterOrphanCheck(f.store.genesisTime)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, true, late)
|
||||
|
||||
@@ -323,10 +320,10 @@ func TestNode_TimeStampsChecks(t *testing.T) {
|
||||
headRoot, err = f.Head(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, root, headRoot)
|
||||
early, err = f.store.choosePayloadContent(f.store.headNode).arrivedEarly(f.store.genesisTime)
|
||||
early, err = f.store.headNode.arrivedEarly(f.store.genesisTime)
|
||||
require.ErrorContains(t, "invalid timestamp", err)
|
||||
require.Equal(t, true, early)
|
||||
late, err = f.store.choosePayloadContent(f.store.headNode).arrivedAfterOrphanCheck(f.store.genesisTime)
|
||||
late, err = f.store.headNode.arrivedAfterOrphanCheck(f.store.genesisTime)
|
||||
require.ErrorContains(t, "invalid timestamp", err)
|
||||
require.Equal(t, false, late)
|
||||
}
|
||||
|
||||
@@ -7,141 +7,93 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// setOptimisticToInvalid removes invalid nodes from forkchoice. It does NOT remove the empty node for the passed root.
|
||||
func (s *Store) setOptimisticToInvalid(ctx context.Context, root, parentRoot, parentHash, lastValidHash [32]byte) ([][32]byte, error) {
|
||||
func (s *Store) setOptimisticToInvalid(ctx context.Context, root, parentRoot, lastValidHash [32]byte) ([][32]byte, error) {
|
||||
invalidRoots := make([][32]byte, 0)
|
||||
n := s.fullNodeByRoot[root]
|
||||
if n == nil {
|
||||
// The offending node with its payload is not in forkchoice. Try with the parent
|
||||
n = s.emptyNodeByRoot[parentRoot]
|
||||
if n == nil {
|
||||
return invalidRoots, errors.Wrap(ErrNilNode, "could not set node to invalid, could not find consensus parent")
|
||||
node, ok := s.nodeByRoot[root]
|
||||
if !ok {
|
||||
node, ok = s.nodeByRoot[parentRoot]
|
||||
if !ok || node == nil {
|
||||
return invalidRoots, errors.Wrap(ErrNilNode, "could not set node to invalid")
|
||||
}
|
||||
if n.node.blockHash == lastValidHash {
|
||||
// The parent node must have been full and with a valid payload
|
||||
// return early if the parent is LVH
|
||||
if node.payloadHash == lastValidHash {
|
||||
return invalidRoots, nil
|
||||
}
|
||||
if n.node.blockHash == parentHash {
|
||||
// The parent was full and invalid
|
||||
n = s.fullNodeByRoot[parentRoot]
|
||||
if n == nil {
|
||||
return invalidRoots, errors.Wrap(ErrNilNode, "could not set node to invalid, could not find full parent")
|
||||
}
|
||||
} else {
|
||||
// The parent is empty and we don't yet know if it's valid or not
|
||||
for n = n.node.parent; n != nil; n = n.node.parent {
|
||||
if ctx.Err() != nil {
|
||||
return invalidRoots, ctx.Err()
|
||||
}
|
||||
if n.node.blockHash == lastValidHash {
|
||||
// The node built on empty and the whole chain was valid
|
||||
return invalidRoots, nil
|
||||
}
|
||||
if n.node.blockHash == parentHash {
|
||||
// The parent was full and invalid
|
||||
break
|
||||
}
|
||||
}
|
||||
if n == nil {
|
||||
return nil, errors.Wrap(ErrNilNode, "could not set node to invalid, could not find full parent in ancestry")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// check consistency with the parent information
|
||||
if n.node.parent == nil {
|
||||
return nil, ErrNilNode
|
||||
if node == nil {
|
||||
return invalidRoots, errors.Wrap(ErrNilNode, "could not set node to invalid")
|
||||
}
|
||||
if n.node.parent.node.root != parentRoot {
|
||||
return nil, errInvalidParentRoot
|
||||
if node.parent.root != parentRoot {
|
||||
return invalidRoots, errInvalidParentRoot
|
||||
}
|
||||
}
|
||||
// n points to a full node that has an invalid payload in forkchoice. We need to find the fist node in the chain that is actually invalid.
|
||||
startNode := n
|
||||
fp := s.fullParent(n)
|
||||
for ; fp != nil && fp.node.blockHash != lastValidHash; fp = s.fullParent(fp) {
|
||||
firstInvalid := node
|
||||
for ; firstInvalid.parent != nil && firstInvalid.parent.payloadHash != lastValidHash; firstInvalid = firstInvalid.parent {
|
||||
if ctx.Err() != nil {
|
||||
return invalidRoots, ctx.Err()
|
||||
}
|
||||
n = fp
|
||||
}
|
||||
// Deal with the case that the last valid payload is in a different fork
|
||||
// This means we are dealing with an EE that does not follow the spec
|
||||
if fp == nil {
|
||||
if firstInvalid.parent == nil {
|
||||
// return early if the invalid node was not imported
|
||||
if startNode.node.root != root {
|
||||
if node.root == parentRoot {
|
||||
return invalidRoots, nil
|
||||
}
|
||||
// Remove just the imported invalid root
|
||||
n = startNode
|
||||
firstInvalid = node
|
||||
}
|
||||
return s.removeNode(ctx, n)
|
||||
return s.removeNode(ctx, firstInvalid)
|
||||
}
|
||||
|
||||
// removeNode removes the node with the given root and all of its children
|
||||
// from the Fork Choice Store.
|
||||
func (s *Store) removeNode(ctx context.Context, pn *PayloadNode) ([][32]byte, error) {
|
||||
func (s *Store) removeNode(ctx context.Context, node *Node) ([][32]byte, error) {
|
||||
invalidRoots := make([][32]byte, 0)
|
||||
|
||||
if pn == nil {
|
||||
if node == nil {
|
||||
return invalidRoots, errors.Wrap(ErrNilNode, "could not remove node")
|
||||
}
|
||||
if !pn.optimistic || pn.node.parent == nil {
|
||||
if !node.optimistic || node.parent == nil {
|
||||
return invalidRoots, errInvalidOptimisticStatus
|
||||
}
|
||||
children := pn.node.parent.children
|
||||
|
||||
children := node.parent.children
|
||||
if len(children) == 1 {
|
||||
pn.node.parent.children = []*Node{}
|
||||
node.parent.children = []*Node{}
|
||||
} else {
|
||||
for i, n := range children {
|
||||
if n == pn.node {
|
||||
if n == node {
|
||||
if i != len(children)-1 {
|
||||
children[i] = children[len(children)-1]
|
||||
}
|
||||
pn.node.parent.children = children[:len(children)-1]
|
||||
node.parent.children = children[:len(children)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return s.removeNodeAndChildren(ctx, pn, invalidRoots)
|
||||
return s.removeNodeAndChildren(ctx, node, invalidRoots)
|
||||
}
|
||||
|
||||
// removeNodeAndChildren removes `node` and all of its descendant from the Store
|
||||
func (s *Store) removeNodeAndChildren(ctx context.Context, pn *PayloadNode, invalidRoots [][32]byte) ([][32]byte, error) {
|
||||
func (s *Store) removeNodeAndChildren(ctx context.Context, node *Node, invalidRoots [][32]byte) ([][32]byte, error) {
|
||||
var err error
|
||||
// If we are removing an empty node, then remove the full node as well if it exists.
|
||||
if !pn.full {
|
||||
fn, ok := s.fullNodeByRoot[pn.node.root]
|
||||
if ok {
|
||||
invalidRoots, err = s.removeNodeAndChildren(ctx, fn, invalidRoots)
|
||||
if err != nil {
|
||||
return invalidRoots, err
|
||||
}
|
||||
}
|
||||
}
|
||||
// Now we remove the full node's children.
|
||||
for _, child := range pn.children {
|
||||
for _, child := range node.children {
|
||||
if ctx.Err() != nil {
|
||||
return invalidRoots, ctx.Err()
|
||||
}
|
||||
// We need to remove only the empty node here since the recursion will take care of the full one.
|
||||
en := s.emptyNodeByRoot[child.root]
|
||||
if invalidRoots, err = s.removeNodeAndChildren(ctx, en, invalidRoots); err != nil {
|
||||
if invalidRoots, err = s.removeNodeAndChildren(ctx, child, invalidRoots); err != nil {
|
||||
return invalidRoots, err
|
||||
}
|
||||
}
|
||||
// Only append the root for the empty nodes.
|
||||
if pn.full {
|
||||
delete(s.fullNodeByRoot, pn.node.root)
|
||||
} else {
|
||||
invalidRoots = append(invalidRoots, pn.node.root)
|
||||
if pn.node.root == s.proposerBoostRoot {
|
||||
s.proposerBoostRoot = [32]byte{}
|
||||
}
|
||||
if pn.node.root == s.previousProposerBoostRoot {
|
||||
s.previousProposerBoostRoot = params.BeaconConfig().ZeroHash
|
||||
s.previousProposerBoostScore = 0
|
||||
}
|
||||
delete(s.emptyNodeByRoot, pn.node.root)
|
||||
invalidRoots = append(invalidRoots, node.root)
|
||||
if node.root == s.proposerBoostRoot {
|
||||
s.proposerBoostRoot = [32]byte{}
|
||||
}
|
||||
if node.root == s.previousProposerBoostRoot {
|
||||
s.previousProposerBoostRoot = params.BeaconConfig().ZeroHash
|
||||
s.previousProposerBoostScore = 0
|
||||
}
|
||||
delete(s.nodeByRoot, node.root)
|
||||
delete(s.nodeByPayload, node.payloadHash)
|
||||
return invalidRoots, nil
|
||||
}
|
||||
|
||||
@@ -23,35 +23,93 @@ import (
|
||||
// And every block in the Fork choice is optimistic.
|
||||
func TestPruneInvalid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
root [32]byte // the root of the new INVALID block
|
||||
parentRoot [32]byte // the root of the parent block
|
||||
parentHash [32]byte // the execution hash of the parent block
|
||||
lastValidHash [32]byte // the last valid execution hash
|
||||
payload [32]byte // the last valid hash
|
||||
wantedNodeNumber int
|
||||
wantedRoots [][32]byte
|
||||
wantedErr error
|
||||
}{
|
||||
{ // Bogus LVH, root not in forkchoice
|
||||
name: "bogus LVH not in forkchoice",
|
||||
root: [32]byte{'x'}, parentRoot: [32]byte{'i'}, parentHash: [32]byte{'I'}, lastValidHash: [32]byte{'R'},
|
||||
wantedNodeNumber: 13, wantedRoots: [][32]byte{},
|
||||
},
|
||||
{ // Bogus LVH
|
||||
name: "bogus LVH",
|
||||
root: [32]byte{'i'}, parentRoot: [32]byte{'h'}, parentHash: [32]byte{'H'}, lastValidHash: [32]byte{'R'},
|
||||
wantedNodeNumber: 13, wantedRoots: [][32]byte{},
|
||||
[32]byte{'x'},
|
||||
[32]byte{'i'},
|
||||
[32]byte{'R'},
|
||||
13,
|
||||
[][32]byte{},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
name: "wanted j",
|
||||
root: [32]byte{'j'}, parentRoot: [32]byte{'b'}, parentHash: [32]byte{'B'}, lastValidHash: [32]byte{'B'},
|
||||
wantedNodeNumber: 13, wantedRoots: [][32]byte{},
|
||||
// Bogus LVH
|
||||
[32]byte{'i'},
|
||||
[32]byte{'h'},
|
||||
[32]byte{'R'},
|
||||
12,
|
||||
[][32]byte{{'i'}},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
name: "wanted 5",
|
||||
root: [32]byte{'c'}, parentRoot: [32]byte{'b'}, parentHash: [32]byte{'B'}, lastValidHash: [32]byte{'B'},
|
||||
wantedNodeNumber: 5,
|
||||
wantedRoots: [][32]byte{
|
||||
[32]byte{'j'},
|
||||
[32]byte{'b'},
|
||||
[32]byte{'B'},
|
||||
12,
|
||||
[][32]byte{{'j'}},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
[32]byte{'c'},
|
||||
[32]byte{'b'},
|
||||
[32]byte{'B'},
|
||||
4,
|
||||
[][32]byte{{'f'}, {'e'}, {'i'}, {'h'}, {'l'},
|
||||
{'k'}, {'g'}, {'d'}, {'c'}},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
[32]byte{'i'},
|
||||
[32]byte{'h'},
|
||||
[32]byte{'H'},
|
||||
12,
|
||||
[][32]byte{{'i'}},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
[32]byte{'h'},
|
||||
[32]byte{'g'},
|
||||
[32]byte{'G'},
|
||||
11,
|
||||
[][32]byte{{'i'}, {'h'}},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
[32]byte{'g'},
|
||||
[32]byte{'d'},
|
||||
[32]byte{'D'},
|
||||
8,
|
||||
[][32]byte{{'i'}, {'h'}, {'l'}, {'k'}, {'g'}},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
[32]byte{'i'},
|
||||
[32]byte{'h'},
|
||||
[32]byte{'D'},
|
||||
8,
|
||||
[][32]byte{{'i'}, {'h'}, {'l'}, {'k'}, {'g'}},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
[32]byte{'f'},
|
||||
[32]byte{'e'},
|
||||
[32]byte{'D'},
|
||||
11,
|
||||
[][32]byte{{'f'}, {'e'}},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
[32]byte{'h'},
|
||||
[32]byte{'g'},
|
||||
[32]byte{'C'},
|
||||
5,
|
||||
[][32]byte{
|
||||
{'f'},
|
||||
{'e'},
|
||||
{'i'},
|
||||
@@ -61,118 +119,106 @@ func TestPruneInvalid(t *testing.T) {
|
||||
{'g'},
|
||||
{'d'},
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
name: "wanted i",
|
||||
root: [32]byte{'i'}, parentRoot: [32]byte{'h'}, parentHash: [32]byte{'H'}, lastValidHash: [32]byte{'H'},
|
||||
wantedNodeNumber: 13, wantedRoots: [][32]byte{},
|
||||
[32]byte{'g'},
|
||||
[32]byte{'d'},
|
||||
[32]byte{'E'},
|
||||
8,
|
||||
[][32]byte{{'i'}, {'h'}, {'l'}, {'k'}, {'g'}},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
name: "wanted i and h",
|
||||
root: [32]byte{'h'}, parentRoot: [32]byte{'g'}, parentHash: [32]byte{'G'}, lastValidHash: [32]byte{'G'},
|
||||
wantedNodeNumber: 12, wantedRoots: [][32]byte{{'i'}},
|
||||
[32]byte{'z'},
|
||||
[32]byte{'j'},
|
||||
[32]byte{'B'},
|
||||
12,
|
||||
[][32]byte{{'j'}},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
name: "wanted i--g",
|
||||
root: [32]byte{'g'}, parentRoot: [32]byte{'d'}, parentHash: [32]byte{'D'}, lastValidHash: [32]byte{'D'},
|
||||
wantedNodeNumber: 9, wantedRoots: [][32]byte{{'i'}, {'h'}, {'l'}, {'k'}},
|
||||
[32]byte{'z'},
|
||||
[32]byte{'j'},
|
||||
[32]byte{'J'},
|
||||
13,
|
||||
[][32]byte{},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
name: "wanted 9",
|
||||
root: [32]byte{'i'}, parentRoot: [32]byte{'h'}, parentHash: [32]byte{'H'}, lastValidHash: [32]byte{'D'},
|
||||
wantedNodeNumber: 9, wantedRoots: [][32]byte{{'i'}, {'h'}, {'l'}, {'k'}},
|
||||
[32]byte{'j'},
|
||||
[32]byte{'a'},
|
||||
[32]byte{'B'},
|
||||
0,
|
||||
[][32]byte{},
|
||||
errInvalidParentRoot,
|
||||
},
|
||||
{
|
||||
name: "wanted f and e",
|
||||
root: [32]byte{'f'}, parentRoot: [32]byte{'e'}, parentHash: [32]byte{'E'}, lastValidHash: [32]byte{'D'},
|
||||
wantedNodeNumber: 12, wantedRoots: [][32]byte{{'f'}},
|
||||
[32]byte{'z'},
|
||||
[32]byte{'h'},
|
||||
[32]byte{'D'},
|
||||
8,
|
||||
[][32]byte{{'i'}, {'h'}, {'l'}, {'k'}, {'g'}},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
name: "wanted 6",
|
||||
root: [32]byte{'h'}, parentRoot: [32]byte{'g'}, parentHash: [32]byte{'G'}, lastValidHash: [32]byte{'C'},
|
||||
wantedNodeNumber: 6,
|
||||
wantedRoots: [][32]byte{
|
||||
{'f'}, {'e'}, {'i'}, {'h'}, {'l'}, {'k'}, {'g'},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wanted 9 again",
|
||||
root: [32]byte{'g'}, parentRoot: [32]byte{'d'}, parentHash: [32]byte{'D'}, lastValidHash: [32]byte{'E'},
|
||||
wantedNodeNumber: 9, wantedRoots: [][32]byte{{'i'}, {'h'}, {'l'}, {'k'}},
|
||||
},
|
||||
{
|
||||
name: "wanted 13",
|
||||
root: [32]byte{'z'}, parentRoot: [32]byte{'j'}, parentHash: [32]byte{'J'}, lastValidHash: [32]byte{'B'},
|
||||
wantedNodeNumber: 13, wantedRoots: [][32]byte{},
|
||||
},
|
||||
{
|
||||
name: "wanted empty",
|
||||
root: [32]byte{'z'}, parentRoot: [32]byte{'j'}, parentHash: [32]byte{'J'}, lastValidHash: [32]byte{'J'},
|
||||
wantedNodeNumber: 13, wantedRoots: [][32]byte{},
|
||||
},
|
||||
{
|
||||
name: "errInvalidParentRoot",
|
||||
root: [32]byte{'j'}, parentRoot: [32]byte{'a'}, parentHash: [32]byte{'A'}, lastValidHash: [32]byte{'B'},
|
||||
wantedErr: errInvalidParentRoot,
|
||||
},
|
||||
{
|
||||
name: "root z",
|
||||
root: [32]byte{'z'}, parentRoot: [32]byte{'h'}, parentHash: [32]byte{'H'}, lastValidHash: [32]byte{'D'},
|
||||
wantedNodeNumber: 9, wantedRoots: [][32]byte{{'i'}, {'h'}, {'l'}, {'k'}},
|
||||
[32]byte{'z'},
|
||||
[32]byte{'h'},
|
||||
[32]byte{'D'},
|
||||
8,
|
||||
[][32]byte{{'i'}, {'h'}, {'l'}, {'k'}, {'g'}},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
f := setup(1, 1)
|
||||
require.NoError(t, f.SetOptimisticToValid(ctx, [32]byte{}))
|
||||
state, blkRoot, err := prepareForkchoiceState(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 102, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 102, [32]byte{'j'}, [32]byte{'b'}, [32]byte{'J'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 103, [32]byte{'d'}, [32]byte{'c'}, [32]byte{'D'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 104, [32]byte{'e'}, [32]byte{'d'}, [32]byte{'E'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 104, [32]byte{'g'}, [32]byte{'d'}, [32]byte{'G'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 105, [32]byte{'f'}, [32]byte{'e'}, [32]byte{'F'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 105, [32]byte{'h'}, [32]byte{'g'}, [32]byte{'H'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 105, [32]byte{'k'}, [32]byte{'g'}, [32]byte{'K'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 106, [32]byte{'i'}, [32]byte{'h'}, [32]byte{'I'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 106, [32]byte{'l'}, [32]byte{'k'}, [32]byte{'L'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
ctx := t.Context()
|
||||
f := setup(1, 1)
|
||||
|
||||
roots, err := f.store.setOptimisticToInvalid(t.Context(), tc.root, tc.parentRoot, tc.parentHash, tc.lastValidHash)
|
||||
if tc.wantedErr == nil {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(tc.wantedRoots), len(roots))
|
||||
require.DeepEqual(t, tc.wantedRoots, roots)
|
||||
require.Equal(t, tc.wantedNodeNumber, f.NodeCount())
|
||||
} else {
|
||||
require.ErrorIs(t, tc.wantedErr, err)
|
||||
}
|
||||
})
|
||||
state, blkRoot, err := prepareForkchoiceState(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 102, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 102, [32]byte{'j'}, [32]byte{'b'}, [32]byte{'J'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 103, [32]byte{'d'}, [32]byte{'c'}, [32]byte{'D'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 104, [32]byte{'e'}, [32]byte{'d'}, [32]byte{'E'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 104, [32]byte{'g'}, [32]byte{'d'}, [32]byte{'G'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 105, [32]byte{'f'}, [32]byte{'e'}, [32]byte{'F'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 105, [32]byte{'h'}, [32]byte{'g'}, [32]byte{'H'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 105, [32]byte{'k'}, [32]byte{'g'}, [32]byte{'K'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 106, [32]byte{'i'}, [32]byte{'h'}, [32]byte{'I'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 106, [32]byte{'l'}, [32]byte{'k'}, [32]byte{'L'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
|
||||
roots, err := f.store.setOptimisticToInvalid(t.Context(), tc.root, tc.parentRoot, tc.payload)
|
||||
if tc.wantedErr == nil {
|
||||
require.NoError(t, err)
|
||||
require.DeepEqual(t, tc.wantedRoots, roots)
|
||||
require.Equal(t, tc.wantedNodeNumber, f.NodeCount())
|
||||
} else {
|
||||
require.ErrorIs(t, tc.wantedErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,40 +240,11 @@ func TestSetOptimisticToInvalid_ProposerBoost(t *testing.T) {
|
||||
f.store.previousProposerBoostScore = 10
|
||||
f.store.previousProposerBoostRoot = [32]byte{'b'}
|
||||
|
||||
_, err = f.SetOptimisticToInvalid(ctx, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'B'}, [32]byte{'A'})
|
||||
_, err = f.SetOptimisticToInvalid(ctx, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'A'})
|
||||
require.NoError(t, err)
|
||||
// proposer boost is still applied to c
|
||||
require.Equal(t, uint64(10), f.store.previousProposerBoostScore)
|
||||
require.Equal(t, [32]byte{}, f.store.proposerBoostRoot)
|
||||
require.Equal(t, [32]byte{'b'}, f.store.previousProposerBoostRoot)
|
||||
}
|
||||
|
||||
func TestSetOptimisticToInvalid_ProposerBoost_Older(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
f := setup(1, 1)
|
||||
|
||||
state, blkRoot, err := prepareForkchoiceState(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 102, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 103, [32]byte{'d'}, [32]byte{'c'}, [32]byte{'D'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
f.store.proposerBoostRoot = [32]byte{'d'}
|
||||
f.store.previousProposerBoostScore = 10
|
||||
f.store.previousProposerBoostRoot = [32]byte{'c'}
|
||||
|
||||
_, err = f.SetOptimisticToInvalid(ctx, [32]byte{'d'}, [32]byte{'c'}, [32]byte{'C'}, [32]byte{'A'})
|
||||
require.NoError(t, err)
|
||||
// proposer boost is still applied to c
|
||||
require.Equal(t, uint64(0), f.store.previousProposerBoostScore)
|
||||
require.Equal(t, [32]byte{}, f.store.proposerBoostRoot)
|
||||
require.Equal(t, [32]byte{}, f.store.previousProposerBoostRoot)
|
||||
require.DeepEqual(t, [32]byte{}, f.store.proposerBoostRoot)
|
||||
require.DeepEqual(t, params.BeaconConfig().ZeroHash, f.store.previousProposerBoostRoot)
|
||||
}
|
||||
|
||||
// This is a regression test (10565)
|
||||
@@ -255,9 +272,10 @@ func TestSetOptimisticToInvalid_CorrectChildren(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
|
||||
_, err = f.store.setOptimisticToInvalid(ctx, [32]byte{'d'}, [32]byte{'a'}, [32]byte{'A'}, [32]byte{'A'})
|
||||
_, err = f.store.setOptimisticToInvalid(ctx, [32]byte{'d'}, [32]byte{'a'}, [32]byte{'A'})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(f.store.fullNodeByRoot[[32]byte{'a'}].children))
|
||||
require.Equal(t, 2, len(f.store.nodeByRoot[[32]byte{'a'}].children))
|
||||
|
||||
}
|
||||
|
||||
// Pow | Pos
|
||||
@@ -304,13 +322,13 @@ func TestSetOptimisticToInvalid_ForkAtMerge(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, st, root))
|
||||
|
||||
roots, err := f.SetOptimisticToInvalid(ctx, [32]byte{'x'}, [32]byte{'d'}, [32]byte{'D'}, [32]byte{})
|
||||
roots, err := f.SetOptimisticToInvalid(ctx, [32]byte{'x'}, [32]byte{'d'}, [32]byte{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, len(roots))
|
||||
require.Equal(t, 4, len(roots))
|
||||
sort.Slice(roots, func(i, j int) bool {
|
||||
return bytesutil.BytesToUint64BigEndian(roots[i][:]) < bytesutil.BytesToUint64BigEndian(roots[j][:])
|
||||
})
|
||||
require.DeepEqual(t, roots, [][32]byte{{'c'}, {'d'}, {'e'}})
|
||||
require.DeepEqual(t, roots, [][32]byte{{'b'}, {'c'}, {'d'}, {'e'}})
|
||||
}
|
||||
|
||||
// Pow | Pos
|
||||
@@ -357,13 +375,13 @@ func TestSetOptimisticToInvalid_ForkAtMerge_bis(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, st, root))
|
||||
|
||||
roots, err := f.SetOptimisticToInvalid(ctx, [32]byte{'x'}, [32]byte{'d'}, [32]byte{'D'}, [32]byte{})
|
||||
roots, err := f.SetOptimisticToInvalid(ctx, [32]byte{'x'}, [32]byte{'d'}, [32]byte{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, len(roots))
|
||||
require.Equal(t, 4, len(roots))
|
||||
sort.Slice(roots, func(i, j int) bool {
|
||||
return bytesutil.BytesToUint64BigEndian(roots[i][:]) < bytesutil.BytesToUint64BigEndian(roots[j][:])
|
||||
})
|
||||
require.DeepEqual(t, roots, [][32]byte{{'c'}, {'d'}, {'e'}})
|
||||
require.DeepEqual(t, roots, [][32]byte{{'b'}, {'c'}, {'d'}, {'e'}})
|
||||
}
|
||||
|
||||
func TestSetOptimisticToValid(t *testing.T) {
|
||||
|
||||
@@ -11,7 +11,7 @@ func (f *ForkChoice) applyProposerBoostScore() error {
|
||||
s := f.store
|
||||
proposerScore := uint64(0)
|
||||
if s.previousProposerBoostRoot != params.BeaconConfig().ZeroHash {
|
||||
previousNode, ok := s.emptyNodeByRoot[s.previousProposerBoostRoot]
|
||||
previousNode, ok := s.nodeByRoot[s.previousProposerBoostRoot]
|
||||
if !ok || previousNode == nil {
|
||||
log.WithError(errInvalidProposerBoostRoot).Errorf("invalid prev root %#x", s.previousProposerBoostRoot)
|
||||
} else {
|
||||
@@ -20,7 +20,7 @@ func (f *ForkChoice) applyProposerBoostScore() error {
|
||||
}
|
||||
|
||||
if s.proposerBoostRoot != params.BeaconConfig().ZeroHash {
|
||||
currentNode, ok := s.emptyNodeByRoot[s.proposerBoostRoot]
|
||||
currentNode, ok := s.nodeByRoot[s.proposerBoostRoot]
|
||||
if !ok || currentNode == nil {
|
||||
log.WithError(errInvalidProposerBoostRoot).Errorf("invalid current root %#x", s.proposerBoostRoot)
|
||||
} else {
|
||||
|
||||
@@ -166,14 +166,14 @@ func TestForkChoice_BoostProposerRoot_PreventsExAnteAttack(t *testing.T) {
|
||||
// (1: 48) -> (2: 38) -> (3: 10)
|
||||
// \--------------->(4: 18)
|
||||
//
|
||||
node1 := f.store.emptyNodeByRoot[indexToHash(1)]
|
||||
require.Equal(t, node1.node.weight, uint64(48))
|
||||
node2 := f.store.emptyNodeByRoot[indexToHash(2)]
|
||||
require.Equal(t, node2.node.weight, uint64(38))
|
||||
node3 := f.store.emptyNodeByRoot[indexToHash(3)]
|
||||
require.Equal(t, node3.node.weight, uint64(10))
|
||||
node4 := f.store.emptyNodeByRoot[indexToHash(4)]
|
||||
require.Equal(t, node4.node.weight, uint64(18))
|
||||
node1 := f.store.nodeByRoot[indexToHash(1)]
|
||||
require.Equal(t, node1.weight, uint64(48))
|
||||
node2 := f.store.nodeByRoot[indexToHash(2)]
|
||||
require.Equal(t, node2.weight, uint64(38))
|
||||
node3 := f.store.nodeByRoot[indexToHash(3)]
|
||||
require.Equal(t, node3.weight, uint64(10))
|
||||
node4 := f.store.nodeByRoot[indexToHash(4)]
|
||||
require.Equal(t, node4.weight, uint64(18))
|
||||
|
||||
// Regression: process attestations for C, check that it
|
||||
// becomes head, we need two attestations to have C.weight = 30 > 24 = D.weight
|
||||
|
||||
@@ -34,23 +34,22 @@ const orphanLateBlockProposingEarly = 2
|
||||
func (f *ForkChoice) ShouldOverrideFCU() (override bool) {
|
||||
override = false
|
||||
|
||||
// We only need to override FCU if our current consensusHead is from the current
|
||||
// We only need to override FCU if our current head is from the current
|
||||
// slot. This differs from the spec implementation in that we assume
|
||||
// that we will call this function in the previous slot to proposing.
|
||||
consensusHead := f.store.headNode
|
||||
if consensusHead == nil {
|
||||
head := f.store.headNode
|
||||
if head == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if consensusHead.slot != slots.CurrentSlot(f.store.genesisTime) {
|
||||
if head.slot != slots.CurrentSlot(f.store.genesisTime) {
|
||||
return
|
||||
}
|
||||
|
||||
// Do not reorg on epoch boundaries
|
||||
if (consensusHead.slot+1)%params.BeaconConfig().SlotsPerEpoch == 0 {
|
||||
if (head.slot+1)%params.BeaconConfig().SlotsPerEpoch == 0 {
|
||||
return
|
||||
}
|
||||
head := f.store.choosePayloadContent(consensusHead)
|
||||
// Only reorg blocks that arrive late
|
||||
early, err := head.arrivedEarly(f.store.genesisTime)
|
||||
if err != nil {
|
||||
@@ -62,15 +61,15 @@ func (f *ForkChoice) ShouldOverrideFCU() (override bool) {
|
||||
}
|
||||
// Only reorg if we have been finalizing
|
||||
finalizedEpoch := f.store.finalizedCheckpoint.Epoch
|
||||
if slots.ToEpoch(consensusHead.slot+1) > finalizedEpoch+params.BeaconConfig().ReorgMaxEpochsSinceFinalization {
|
||||
if slots.ToEpoch(head.slot+1) > finalizedEpoch+params.BeaconConfig().ReorgMaxEpochsSinceFinalization {
|
||||
return
|
||||
}
|
||||
// Only orphan a single block
|
||||
parent := consensusHead.parent
|
||||
parent := head.parent
|
||||
if parent == nil {
|
||||
return
|
||||
}
|
||||
if consensusHead.slot > parent.node.slot+1 {
|
||||
if head.slot > parent.slot+1 {
|
||||
return
|
||||
}
|
||||
// Do not orphan a block that has higher justification than the parent
|
||||
@@ -79,12 +78,12 @@ func (f *ForkChoice) ShouldOverrideFCU() (override bool) {
|
||||
// }
|
||||
|
||||
// Only orphan a block if the head LMD vote is weak
|
||||
if consensusHead.weight*100 > f.store.committeeWeight*params.BeaconConfig().ReorgHeadWeightThreshold {
|
||||
if head.weight*100 > f.store.committeeWeight*params.BeaconConfig().ReorgHeadWeightThreshold {
|
||||
return
|
||||
}
|
||||
|
||||
// Return early if we are checking before 10 seconds into the slot
|
||||
sss, err := slots.SinceSlotStart(consensusHead.slot, f.store.genesisTime, time.Now())
|
||||
sss, err := slots.SinceSlotStart(head.slot, f.store.genesisTime, time.Now())
|
||||
if err != nil {
|
||||
log.WithError(err).Error("could not check current slot")
|
||||
return true
|
||||
@@ -93,7 +92,7 @@ func (f *ForkChoice) ShouldOverrideFCU() (override bool) {
|
||||
return true
|
||||
}
|
||||
// Only orphan a block if the parent LMD vote is strong
|
||||
if parent.node.weight*100 < f.store.committeeWeight*params.BeaconConfig().ReorgParentWeightThreshold {
|
||||
if parent.weight*100 < f.store.committeeWeight*params.BeaconConfig().ReorgParentWeightThreshold {
|
||||
return
|
||||
}
|
||||
return true
|
||||
@@ -107,61 +106,60 @@ func (f *ForkChoice) ShouldOverrideFCU() (override bool) {
|
||||
// This function needs to be called only when proposing a block and all
|
||||
// attestation processing has already happened.
|
||||
func (f *ForkChoice) GetProposerHead() [32]byte {
|
||||
consensusHead := f.store.headNode
|
||||
if consensusHead == nil {
|
||||
head := f.store.headNode
|
||||
if head == nil {
|
||||
return [32]byte{}
|
||||
}
|
||||
// Only reorg blocks from the previous slot.
|
||||
currentSlot := slots.CurrentSlot(f.store.genesisTime)
|
||||
if consensusHead.slot+1 != currentSlot {
|
||||
return consensusHead.root
|
||||
if head.slot+1 != currentSlot {
|
||||
return head.root
|
||||
}
|
||||
// Do not reorg on epoch boundaries
|
||||
if (consensusHead.slot+1)%params.BeaconConfig().SlotsPerEpoch == 0 {
|
||||
return consensusHead.root
|
||||
if (head.slot+1)%params.BeaconConfig().SlotsPerEpoch == 0 {
|
||||
return head.root
|
||||
}
|
||||
// Only reorg blocks that arrive late
|
||||
head := f.store.choosePayloadContent(consensusHead)
|
||||
early, err := head.arrivedEarly(f.store.genesisTime)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("could not check if block arrived early")
|
||||
return consensusHead.root
|
||||
return head.root
|
||||
}
|
||||
if early {
|
||||
return consensusHead.root
|
||||
return head.root
|
||||
}
|
||||
// Only reorg if we have been finalizing
|
||||
finalizedEpoch := f.store.finalizedCheckpoint.Epoch
|
||||
if slots.ToEpoch(consensusHead.slot+1) > finalizedEpoch+params.BeaconConfig().ReorgMaxEpochsSinceFinalization {
|
||||
return consensusHead.root
|
||||
if slots.ToEpoch(head.slot+1) > finalizedEpoch+params.BeaconConfig().ReorgMaxEpochsSinceFinalization {
|
||||
return head.root
|
||||
}
|
||||
// Only orphan a single block
|
||||
parent := consensusHead.parent
|
||||
parent := head.parent
|
||||
if parent == nil {
|
||||
return consensusHead.root
|
||||
return head.root
|
||||
}
|
||||
if consensusHead.slot > parent.node.slot+1 {
|
||||
return consensusHead.root
|
||||
if head.slot > parent.slot+1 {
|
||||
return head.root
|
||||
}
|
||||
|
||||
// Only orphan a block if the head LMD vote is weak
|
||||
if consensusHead.weight*100 > f.store.committeeWeight*params.BeaconConfig().ReorgHeadWeightThreshold {
|
||||
return consensusHead.root
|
||||
if head.weight*100 > f.store.committeeWeight*params.BeaconConfig().ReorgHeadWeightThreshold {
|
||||
return head.root
|
||||
}
|
||||
|
||||
// Only orphan a block if the parent LMD vote is strong
|
||||
if parent.node.weight*100 < f.store.committeeWeight*params.BeaconConfig().ReorgParentWeightThreshold {
|
||||
return consensusHead.root
|
||||
if parent.weight*100 < f.store.committeeWeight*params.BeaconConfig().ReorgParentWeightThreshold {
|
||||
return head.root
|
||||
}
|
||||
|
||||
// Only reorg if we are proposing early
|
||||
sss, err := slots.SinceSlotStart(currentSlot, f.store.genesisTime, time.Now())
|
||||
if err != nil {
|
||||
log.WithError(err).Error("could not check if proposing early")
|
||||
return consensusHead.root
|
||||
return head.root
|
||||
}
|
||||
if sss >= orphanLateBlockProposingEarly*time.Second {
|
||||
return consensusHead.root
|
||||
return head.root
|
||||
}
|
||||
return parent.node.root
|
||||
return parent.root
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ func TestForkChoice_ShouldOverrideFCU(t *testing.T) {
|
||||
require.Equal(t, blk.Root(), headRoot)
|
||||
t.Run("head is weak", func(t *testing.T) {
|
||||
require.Equal(t, true, f.ShouldOverrideFCU())
|
||||
|
||||
})
|
||||
t.Run("head is nil", func(t *testing.T) {
|
||||
saved := f.store.headNode
|
||||
@@ -59,11 +60,10 @@ func TestForkChoice_ShouldOverrideFCU(t *testing.T) {
|
||||
f.store.headNode.slot = saved
|
||||
})
|
||||
t.Run("head is early", func(t *testing.T) {
|
||||
fn := f.store.fullNodeByRoot[f.store.headNode.root]
|
||||
saved := fn.timestamp
|
||||
fn.timestamp = saved.Add(-2 * time.Second)
|
||||
saved := f.store.headNode.timestamp
|
||||
f.store.headNode.timestamp = saved.Add(-2 * time.Second)
|
||||
require.Equal(t, false, f.ShouldOverrideFCU())
|
||||
fn.timestamp = saved
|
||||
f.store.headNode.timestamp = saved
|
||||
})
|
||||
t.Run("chain not finalizing", func(t *testing.T) {
|
||||
saved := f.store.headNode.slot
|
||||
@@ -74,10 +74,10 @@ func TestForkChoice_ShouldOverrideFCU(t *testing.T) {
|
||||
driftGenesisTime(f, 2, orphanLateBlockFirstThreshold+time.Second)
|
||||
})
|
||||
t.Run("Not single block reorg", func(t *testing.T) {
|
||||
saved := f.store.headNode.parent.node.slot
|
||||
f.store.headNode.parent.node.slot = 0
|
||||
saved := f.store.headNode.parent.slot
|
||||
f.store.headNode.parent.slot = 0
|
||||
require.Equal(t, false, f.ShouldOverrideFCU())
|
||||
f.store.headNode.parent.node.slot = saved
|
||||
f.store.headNode.parent.slot = saved
|
||||
})
|
||||
t.Run("parent is nil", func(t *testing.T) {
|
||||
saved := f.store.headNode.parent
|
||||
@@ -86,17 +86,17 @@ func TestForkChoice_ShouldOverrideFCU(t *testing.T) {
|
||||
f.store.headNode.parent = saved
|
||||
})
|
||||
t.Run("parent is weak early call", func(t *testing.T) {
|
||||
saved := f.store.headNode.parent.node.weight
|
||||
f.store.headNode.parent.node.weight = 0
|
||||
saved := f.store.headNode.parent.weight
|
||||
f.store.headNode.parent.weight = 0
|
||||
require.Equal(t, true, f.ShouldOverrideFCU())
|
||||
f.store.headNode.parent.node.weight = saved
|
||||
f.store.headNode.parent.weight = saved
|
||||
})
|
||||
t.Run("parent is weak late call", func(t *testing.T) {
|
||||
saved := f.store.headNode.parent.node.weight
|
||||
saved := f.store.headNode.parent.weight
|
||||
driftGenesisTime(f, 2, 11*time.Second)
|
||||
f.store.headNode.parent.node.weight = 0
|
||||
f.store.headNode.parent.weight = 0
|
||||
require.Equal(t, false, f.ShouldOverrideFCU())
|
||||
f.store.headNode.parent.node.weight = saved
|
||||
f.store.headNode.parent.weight = saved
|
||||
driftGenesisTime(f, 2, orphanLateBlockFirstThreshold+time.Second)
|
||||
})
|
||||
t.Run("Head is strong", func(t *testing.T) {
|
||||
@@ -135,8 +135,7 @@ func TestForkChoice_GetProposerHead(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, blk.Root(), headRoot)
|
||||
orphanLateBlockFirstThreshold := params.BeaconConfig().SlotComponentDuration(params.BeaconConfig().AttestationDueBPS)
|
||||
fn := f.store.fullNodeByRoot[f.store.headNode.root]
|
||||
fn.timestamp = fn.timestamp.Add(-1 * (params.BeaconConfig().SlotDuration() - orphanLateBlockFirstThreshold))
|
||||
f.store.headNode.timestamp.Add(-1 * (params.BeaconConfig().SlotDuration() - orphanLateBlockFirstThreshold))
|
||||
t.Run("head is weak", func(t *testing.T) {
|
||||
require.Equal(t, parentRoot, f.GetProposerHead())
|
||||
})
|
||||
@@ -160,12 +159,11 @@ func TestForkChoice_GetProposerHead(t *testing.T) {
|
||||
f.store.headNode.slot = saved
|
||||
})
|
||||
t.Run("head is early", func(t *testing.T) {
|
||||
fn := f.store.fullNodeByRoot[f.store.headNode.root]
|
||||
saved := fn.timestamp
|
||||
saved := f.store.headNode.timestamp
|
||||
headTimeStamp := f.store.genesisTime.Add(time.Duration(uint64(f.store.headNode.slot)*params.BeaconConfig().SecondsPerSlot+1) * time.Second)
|
||||
fn.timestamp = headTimeStamp
|
||||
f.store.headNode.timestamp = headTimeStamp
|
||||
require.Equal(t, childRoot, f.GetProposerHead())
|
||||
fn.timestamp = saved
|
||||
f.store.headNode.timestamp = saved
|
||||
})
|
||||
t.Run("chain not finalizing", func(t *testing.T) {
|
||||
saved := f.store.headNode.slot
|
||||
@@ -176,10 +174,10 @@ func TestForkChoice_GetProposerHead(t *testing.T) {
|
||||
driftGenesisTime(f, 3, 1*time.Second)
|
||||
})
|
||||
t.Run("Not single block reorg", func(t *testing.T) {
|
||||
saved := f.store.headNode.parent.node.slot
|
||||
f.store.headNode.parent.node.slot = 0
|
||||
saved := f.store.headNode.parent.slot
|
||||
f.store.headNode.parent.slot = 0
|
||||
require.Equal(t, childRoot, f.GetProposerHead())
|
||||
f.store.headNode.parent.node.slot = saved
|
||||
f.store.headNode.parent.slot = saved
|
||||
})
|
||||
t.Run("parent is nil", func(t *testing.T) {
|
||||
saved := f.store.headNode.parent
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/OffchainLabs/prysm/v7/runtime/version"
|
||||
"github.com/OffchainLabs/prysm/v7/time/slots"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// head starts from justified root and then follows the best descendant links
|
||||
@@ -27,16 +26,13 @@ func (s *Store) head(ctx context.Context) ([32]byte, error) {
|
||||
}
|
||||
|
||||
// JustifiedRoot has to be known
|
||||
var jn *Node
|
||||
ej := s.emptyNodeByRoot[s.justifiedCheckpoint.Root]
|
||||
if ej != nil {
|
||||
jn = ej.node
|
||||
} else {
|
||||
justifiedNode, ok := s.nodeByRoot[s.justifiedCheckpoint.Root]
|
||||
if !ok || justifiedNode == nil {
|
||||
// If the justifiedCheckpoint is from genesis, then the root is
|
||||
// zeroHash. In this case it should be the root of forkchoice
|
||||
// tree.
|
||||
if s.justifiedCheckpoint.Epoch == params.BeaconConfig().GenesisEpoch {
|
||||
jn = s.treeRootNode
|
||||
justifiedNode = s.treeRootNode
|
||||
} else {
|
||||
return [32]byte{}, errors.WithMessage(errUnknownJustifiedRoot, fmt.Sprintf("%#x", s.justifiedCheckpoint.Root))
|
||||
}
|
||||
@@ -44,9 +40,9 @@ func (s *Store) head(ctx context.Context) ([32]byte, error) {
|
||||
|
||||
// If the justified node doesn't have a best descendant,
|
||||
// the best node is itself.
|
||||
bestDescendant := jn.bestDescendant
|
||||
bestDescendant := justifiedNode.bestDescendant
|
||||
if bestDescendant == nil {
|
||||
bestDescendant = jn
|
||||
bestDescendant = justifiedNode
|
||||
}
|
||||
currentEpoch := slots.EpochsSinceGenesis(s.genesisTime)
|
||||
if !bestDescendant.viableForHead(s.justifiedCheckpoint.Epoch, currentEpoch) {
|
||||
@@ -70,42 +66,29 @@ func (s *Store) head(ctx context.Context) ([32]byte, error) {
|
||||
// It then updates the new node's parent with the best child and descendant node.
|
||||
func (s *Store) insert(ctx context.Context,
|
||||
roblock consensus_blocks.ROBlock,
|
||||
justifiedEpoch, finalizedEpoch primitives.Epoch,
|
||||
) (*PayloadNode, error) {
|
||||
justifiedEpoch, finalizedEpoch primitives.Epoch) (*Node, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "doublyLinkedForkchoice.insert")
|
||||
defer span.End()
|
||||
|
||||
root := roblock.Root()
|
||||
block := roblock.Block()
|
||||
slot := block.Slot()
|
||||
parentRoot := block.ParentRoot()
|
||||
var payloadHash [32]byte
|
||||
if block.Version() >= version.Bellatrix {
|
||||
execution, err := block.Body().Execution()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
copy(payloadHash[:], execution.BlockHash())
|
||||
}
|
||||
|
||||
// Return if the block has been inserted into Store before.
|
||||
if n, ok := s.emptyNodeByRoot[root]; ok {
|
||||
if n, ok := s.nodeByRoot[root]; ok {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
block := roblock.Block()
|
||||
slot := block.Slot()
|
||||
var parent *PayloadNode
|
||||
blockHash := &[32]byte{}
|
||||
if block.Version() >= version.Gloas {
|
||||
if err := s.resolveParentPayloadStatus(block, &parent, blockHash); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if block.Version() >= version.Bellatrix {
|
||||
execution, err := block.Body().Execution()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
copy(blockHash[:], execution.BlockHash())
|
||||
}
|
||||
parentRoot := block.ParentRoot()
|
||||
en := s.emptyNodeByRoot[parentRoot]
|
||||
parent = s.fullNodeByRoot[parentRoot]
|
||||
if parent == nil && en != nil {
|
||||
// pre-Gloas only full parents are allowed.
|
||||
return nil, errInvalidParentRoot
|
||||
}
|
||||
}
|
||||
|
||||
parent := s.nodeByRoot[parentRoot]
|
||||
n := &Node{
|
||||
slot: slot,
|
||||
root: root,
|
||||
@@ -114,51 +97,32 @@ func (s *Store) insert(ctx context.Context,
|
||||
unrealizedJustifiedEpoch: justifiedEpoch,
|
||||
finalizedEpoch: finalizedEpoch,
|
||||
unrealizedFinalizedEpoch: finalizedEpoch,
|
||||
blockHash: *blockHash,
|
||||
optimistic: true,
|
||||
payloadHash: payloadHash,
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
|
||||
// Set the node's target checkpoint
|
||||
if slot%params.BeaconConfig().SlotsPerEpoch == 0 {
|
||||
n.target = n
|
||||
} else if parent != nil {
|
||||
if slots.ToEpoch(slot) == slots.ToEpoch(parent.node.slot) {
|
||||
n.target = parent.node.target
|
||||
if slots.ToEpoch(slot) == slots.ToEpoch(parent.slot) {
|
||||
n.target = parent.target
|
||||
} else {
|
||||
n.target = parent.node
|
||||
n.target = parent
|
||||
}
|
||||
}
|
||||
var ret *PayloadNode
|
||||
optimistic := true
|
||||
if parent != nil {
|
||||
optimistic = n.parent.optimistic
|
||||
}
|
||||
// Make the empty node.It's optimistic status equals it's parent's status.
|
||||
pn := &PayloadNode{
|
||||
node: n,
|
||||
optimistic: optimistic,
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
s.emptyNodeByRoot[root] = pn
|
||||
ret = pn
|
||||
if block.Version() < version.Gloas {
|
||||
// Make also the full node, this is optimistic until the engine returns the execution payload validation.
|
||||
fn := &PayloadNode{
|
||||
node: n,
|
||||
optimistic: true,
|
||||
timestamp: time.Now(),
|
||||
full: true,
|
||||
}
|
||||
ret = fn
|
||||
s.fullNodeByRoot[root] = fn
|
||||
}
|
||||
|
||||
s.nodeByPayload[payloadHash] = n
|
||||
s.nodeByRoot[root] = n
|
||||
if parent == nil {
|
||||
if s.treeRootNode == nil {
|
||||
s.treeRootNode = n
|
||||
s.headNode = n
|
||||
s.highestReceivedNode = n
|
||||
} else {
|
||||
delete(s.emptyNodeByRoot, root)
|
||||
delete(s.fullNodeByRoot, root)
|
||||
delete(s.nodeByRoot, root)
|
||||
delete(s.nodeByPayload, payloadHash)
|
||||
return nil, errInvalidParentRoot
|
||||
}
|
||||
} else {
|
||||
@@ -166,7 +130,7 @@ func (s *Store) insert(ctx context.Context,
|
||||
// Apply proposer boost
|
||||
now := time.Now()
|
||||
if now.Before(s.genesisTime) {
|
||||
return ret, nil
|
||||
return n, nil
|
||||
}
|
||||
currentSlot := slots.CurrentSlot(s.genesisTime)
|
||||
sss, err := slots.SinceSlotStart(currentSlot, s.genesisTime, now)
|
||||
@@ -182,16 +146,17 @@ func (s *Store) insert(ctx context.Context,
|
||||
// Update best descendants
|
||||
jEpoch := s.justifiedCheckpoint.Epoch
|
||||
fEpoch := s.finalizedCheckpoint.Epoch
|
||||
if err := s.updateBestDescendantConsensusNode(ctx, s.treeRootNode, jEpoch, fEpoch, slots.ToEpoch(currentSlot)); err != nil {
|
||||
log.WithError(err).WithFields(logrus.Fields{
|
||||
"slot": slot,
|
||||
"root": root,
|
||||
}).Error("Could not update best descendant")
|
||||
if err := s.treeRootNode.updateBestDescendant(ctx, jEpoch, fEpoch, slots.ToEpoch(currentSlot)); err != nil {
|
||||
_, remErr := s.removeNode(ctx, n)
|
||||
if remErr != nil {
|
||||
log.WithError(remErr).Error("could not remove node")
|
||||
}
|
||||
return nil, errors.Wrap(err, "could not update best descendants")
|
||||
}
|
||||
}
|
||||
// Update metrics.
|
||||
processedBlockCount.Inc()
|
||||
nodeCount.Set(float64(len(s.emptyNodeByRoot)))
|
||||
nodeCount.Set(float64(len(s.nodeByRoot)))
|
||||
|
||||
// Only update received block slot if it's within epoch from current time.
|
||||
if slot+params.BeaconConfig().SlotsPerEpoch > slots.CurrentSlot(s.genesisTime) {
|
||||
@@ -202,10 +167,10 @@ func (s *Store) insert(ctx context.Context,
|
||||
s.highestReceivedNode = n
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// pruneFinalizedNodeByRootMap prunes the `nodeByRoot` maps
|
||||
// pruneFinalizedNodeByRootMap prunes the `nodeByRoot` map
|
||||
// starting from `node` down to the finalized Node or to a leaf of the Fork
|
||||
// choice store.
|
||||
func (s *Store) pruneFinalizedNodeByRootMap(ctx context.Context, node, finalizedNode *Node) error {
|
||||
@@ -218,51 +183,45 @@ func (s *Store) pruneFinalizedNodeByRootMap(ctx context.Context, node, finalized
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for _, child := range s.allConsensusChildren(node) {
|
||||
for _, child := range node.children {
|
||||
if err := s.pruneFinalizedNodeByRootMap(ctx, child, finalizedNode); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
en := s.emptyNodeByRoot[node.root]
|
||||
en.children = nil
|
||||
delete(s.emptyNodeByRoot, node.root)
|
||||
fn := s.fullNodeByRoot[node.root]
|
||||
if fn != nil {
|
||||
fn.children = nil
|
||||
delete(s.fullNodeByRoot, node.root)
|
||||
}
|
||||
|
||||
node.children = nil
|
||||
delete(s.nodeByRoot, node.root)
|
||||
delete(s.nodeByPayload, node.payloadHash)
|
||||
return nil
|
||||
}
|
||||
|
||||
// prune prunes the fork choice store. It removes all nodes that compete with the finalized root.
|
||||
// This function does not prune for invalid optimistically synced nodes, it deals only with pruning upon finalization
|
||||
// TODO: GLOAS, to ensure that chains up to a full node are found, we may want to consider pruning only up to the latest full block that was finalized
|
||||
func (s *Store) prune(ctx context.Context) error {
|
||||
ctx, span := trace.StartSpan(ctx, "doublyLinkedForkchoice.Prune")
|
||||
defer span.End()
|
||||
|
||||
finalizedRoot := s.finalizedCheckpoint.Root
|
||||
finalizedEpoch := s.finalizedCheckpoint.Epoch
|
||||
fen, ok := s.emptyNodeByRoot[finalizedRoot]
|
||||
if !ok || fen == nil {
|
||||
finalizedNode, ok := s.nodeByRoot[finalizedRoot]
|
||||
if !ok || finalizedNode == nil {
|
||||
return errors.WithMessage(errUnknownFinalizedRoot, fmt.Sprintf("%#x", finalizedRoot))
|
||||
}
|
||||
fn := fen.node
|
||||
// return early if we haven't changed the finalized checkpoint
|
||||
if fn.parent == nil {
|
||||
if finalizedNode.parent == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save the new finalized dependent root because it will be pruned
|
||||
s.finalizedDependentRoot = fn.parent.node.root
|
||||
s.finalizedDependentRoot = finalizedNode.parent.root
|
||||
|
||||
// Prune nodeByRoot starting from root
|
||||
if err := s.pruneFinalizedNodeByRootMap(ctx, s.treeRootNode, fn); err != nil {
|
||||
if err := s.pruneFinalizedNodeByRootMap(ctx, s.treeRootNode, finalizedNode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fn.parent = nil
|
||||
s.treeRootNode = fn
|
||||
finalizedNode.parent = nil
|
||||
s.treeRootNode = finalizedNode
|
||||
|
||||
prunedCount.Inc()
|
||||
// Prune all children of the finalized checkpoint block that are incompatible with it
|
||||
@@ -270,13 +229,13 @@ func (s *Store) prune(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not compute epoch start")
|
||||
}
|
||||
if fn.slot == checkpointMaxSlot {
|
||||
if finalizedNode.slot == checkpointMaxSlot {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, child := range fen.children {
|
||||
for _, child := range finalizedNode.children {
|
||||
if child != nil && child.slot <= checkpointMaxSlot {
|
||||
if err := s.pruneFinalizedNodeByRootMap(ctx, child, fn); err != nil {
|
||||
if err := s.pruneFinalizedNodeByRootMap(ctx, child, finalizedNode); err != nil {
|
||||
return errors.Wrap(err, "could not prune incompatible finalized child")
|
||||
}
|
||||
}
|
||||
@@ -290,10 +249,10 @@ func (s *Store) tips() ([][32]byte, []primitives.Slot) {
|
||||
var roots [][32]byte
|
||||
var slots []primitives.Slot
|
||||
|
||||
for root, n := range s.emptyNodeByRoot {
|
||||
if len(s.allConsensusChildren(n.node)) == 0 {
|
||||
for root, node := range s.nodeByRoot {
|
||||
if len(node.children) == 0 {
|
||||
roots = append(roots, root)
|
||||
slots = append(slots, n.node.slot)
|
||||
slots = append(slots, node.slot)
|
||||
}
|
||||
}
|
||||
return roots, slots
|
||||
@@ -314,6 +273,21 @@ func (f *ForkChoice) HighestReceivedBlockSlot() primitives.Slot {
|
||||
return f.store.highestReceivedNode.slot
|
||||
}
|
||||
|
||||
// HighestReceivedBlockDelay returns the number of slots that the highest
|
||||
// received block was late when receiving it. For example, a block was late by 12 slots,
|
||||
// then this method is expected to return 12.
|
||||
func (f *ForkChoice) HighestReceivedBlockDelay() primitives.Slot {
|
||||
n := f.store.highestReceivedNode
|
||||
if n == nil {
|
||||
return 0
|
||||
}
|
||||
sss, err := slots.SinceSlotStart(n.slot, f.store.genesisTime, n.timestamp)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return primitives.Slot(uint64(sss/time.Second) / params.BeaconConfig().SecondsPerSlot)
|
||||
}
|
||||
|
||||
// ReceivedBlocksLastEpoch returns the number of blocks received in the last epoch
|
||||
func (f *ForkChoice) ReceivedBlocksLastEpoch() (uint64, error) {
|
||||
count := uint64(0)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package doublylinkedtree
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -40,18 +41,18 @@ func TestStore_NodeByRoot(t *testing.T) {
|
||||
state, blkRoot, err = prepareForkchoiceState(t.Context(), 2, indexToHash(2), indexToHash(1), params.BeaconConfig().ZeroHash, 0, 0)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
node0 := f.store.emptyNodeByRoot[params.BeaconConfig().ZeroHash]
|
||||
node1 := f.store.emptyNodeByRoot[indexToHash(1)]
|
||||
node2 := f.store.emptyNodeByRoot[indexToHash(2)]
|
||||
node0 := f.store.treeRootNode
|
||||
node1 := node0.children[0]
|
||||
node2 := node1.children[0]
|
||||
|
||||
expectedRoots := map[[32]byte]*PayloadNode{
|
||||
expectedRoots := map[[32]byte]*Node{
|
||||
params.BeaconConfig().ZeroHash: node0,
|
||||
indexToHash(1): node1,
|
||||
indexToHash(2): node2,
|
||||
}
|
||||
|
||||
require.Equal(t, 3, f.NodeCount())
|
||||
for root, node := range f.store.emptyNodeByRoot {
|
||||
for root, node := range f.store.nodeByRoot {
|
||||
v, ok := expectedRoots[root]
|
||||
require.Equal(t, ok, true)
|
||||
require.Equal(t, v, node)
|
||||
@@ -110,28 +111,38 @@ func TestStore_Head_BestDescendant(t *testing.T) {
|
||||
require.Equal(t, h, indexToHash(4))
|
||||
}
|
||||
|
||||
func TestStore_UpdateBestDescendant_ContextCancelled(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
f := setup(0, 0)
|
||||
state, blkRoot, err := prepareForkchoiceState(ctx, 1, indexToHash(1), params.BeaconConfig().ZeroHash, params.BeaconConfig().ZeroHash, 0, 0)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
cancel()
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 2, indexToHash(2), indexToHash(1), params.BeaconConfig().ZeroHash, 0, 0)
|
||||
require.NoError(t, err)
|
||||
err = f.InsertNode(ctx, state, blkRoot)
|
||||
require.ErrorContains(t, "context canceled", err)
|
||||
}
|
||||
|
||||
func TestStore_Insert(t *testing.T) {
|
||||
// The new node does not have a parent.
|
||||
treeRootNode := &Node{slot: 0, root: indexToHash(0)}
|
||||
emptyRootPN := &PayloadNode{node: treeRootNode}
|
||||
fullRootPN := &PayloadNode{node: treeRootNode, full: true, optimistic: true}
|
||||
emptyNodeByRoot := map[[32]byte]*PayloadNode{indexToHash(0): emptyRootPN}
|
||||
fullNodeByRoot := map[[32]byte]*PayloadNode{indexToHash(0): fullRootPN}
|
||||
nodeByRoot := map[[32]byte]*Node{indexToHash(0): treeRootNode}
|
||||
nodeByPayload := map[[32]byte]*Node{indexToHash(0): treeRootNode}
|
||||
jc := &forkchoicetypes.Checkpoint{Epoch: 0}
|
||||
fc := &forkchoicetypes.Checkpoint{Epoch: 0}
|
||||
s := &Store{emptyNodeByRoot: emptyNodeByRoot, fullNodeByRoot: fullNodeByRoot, treeRootNode: treeRootNode, justifiedCheckpoint: jc, finalizedCheckpoint: fc, highestReceivedNode: &Node{}}
|
||||
s := &Store{nodeByRoot: nodeByRoot, treeRootNode: treeRootNode, nodeByPayload: nodeByPayload, justifiedCheckpoint: jc, finalizedCheckpoint: fc, highestReceivedNode: &Node{}}
|
||||
payloadHash := [32]byte{'a'}
|
||||
ctx := t.Context()
|
||||
_, blk, err := prepareForkchoiceState(ctx, 100, indexToHash(100), indexToHash(0), payloadHash, 1, 1)
|
||||
require.NoError(t, err)
|
||||
_, err = s.insert(ctx, blk, 1, 1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, len(s.emptyNodeByRoot), "Did not insert block")
|
||||
assert.Equal(t, (*PayloadNode)(nil), treeRootNode.parent, "Incorrect parent")
|
||||
children := s.allConsensusChildren(treeRootNode)
|
||||
assert.Equal(t, 1, len(children), "Incorrect children number")
|
||||
assert.Equal(t, payloadHash, children[0].blockHash, "Incorrect payload hash")
|
||||
child := children[0]
|
||||
assert.Equal(t, 2, len(s.nodeByRoot), "Did not insert block")
|
||||
assert.Equal(t, (*Node)(nil), treeRootNode.parent, "Incorrect parent")
|
||||
assert.Equal(t, 1, len(treeRootNode.children), "Incorrect children number")
|
||||
assert.Equal(t, payloadHash, treeRootNode.children[0].payloadHash, "Incorrect payload hash")
|
||||
child := treeRootNode.children[0]
|
||||
assert.Equal(t, primitives.Epoch(1), child.justifiedEpoch, "Incorrect justification")
|
||||
assert.Equal(t, primitives.Epoch(1), child.finalizedEpoch, "Incorrect finalization")
|
||||
assert.Equal(t, indexToHash(100), child.root, "Incorrect root")
|
||||
@@ -156,7 +167,7 @@ func TestStore_Prune_MoreThanThreshold(t *testing.T) {
|
||||
// Finalized root is at index 99 so everything before 99 should be pruned.
|
||||
s.finalizedCheckpoint.Root = indexToHash(99)
|
||||
require.NoError(t, s.prune(t.Context()))
|
||||
assert.Equal(t, 1, len(s.emptyNodeByRoot), "Incorrect nodes count")
|
||||
assert.Equal(t, 1, len(s.nodeByRoot), "Incorrect nodes count")
|
||||
}
|
||||
|
||||
func TestStore_Prune_MoreThanOnce(t *testing.T) {
|
||||
@@ -178,12 +189,12 @@ func TestStore_Prune_MoreThanOnce(t *testing.T) {
|
||||
// Finalized root is at index 11 so everything before 11 should be pruned.
|
||||
s.finalizedCheckpoint.Root = indexToHash(10)
|
||||
require.NoError(t, s.prune(t.Context()))
|
||||
assert.Equal(t, 90, len(s.emptyNodeByRoot), "Incorrect nodes count")
|
||||
assert.Equal(t, 90, len(s.nodeByRoot), "Incorrect nodes count")
|
||||
|
||||
// One more time.
|
||||
s.finalizedCheckpoint.Root = indexToHash(20)
|
||||
require.NoError(t, s.prune(t.Context()))
|
||||
assert.Equal(t, 80, len(s.emptyNodeByRoot), "Incorrect nodes count")
|
||||
assert.Equal(t, 80, len(s.nodeByRoot), "Incorrect nodes count")
|
||||
}
|
||||
|
||||
func TestStore_Prune_ReturnEarly(t *testing.T) {
|
||||
@@ -226,7 +237,8 @@ func TestStore_Prune_NoDanglingBranch(t *testing.T) {
|
||||
s := f.store
|
||||
s.finalizedCheckpoint.Root = indexToHash(1)
|
||||
require.NoError(t, s.prune(t.Context()))
|
||||
require.Equal(t, len(s.emptyNodeByRoot), 1)
|
||||
require.Equal(t, len(s.nodeByRoot), 1)
|
||||
require.Equal(t, len(s.nodeByPayload), 1)
|
||||
}
|
||||
|
||||
// This test starts with the following branching diagram
|
||||
@@ -306,7 +318,9 @@ func TestStore_PruneMapsNodes(t *testing.T) {
|
||||
s := f.store
|
||||
s.finalizedCheckpoint.Root = indexToHash(1)
|
||||
require.NoError(t, s.prune(t.Context()))
|
||||
require.Equal(t, len(s.emptyNodeByRoot), 1)
|
||||
require.Equal(t, len(s.nodeByRoot), 1)
|
||||
require.Equal(t, len(s.nodeByPayload), 1)
|
||||
|
||||
}
|
||||
|
||||
func TestForkChoice_ReceivedBlocksLastEpoch(t *testing.T) {
|
||||
@@ -325,6 +339,7 @@ func TestForkChoice_ReceivedBlocksLastEpoch(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), count)
|
||||
require.Equal(t, primitives.Slot(1), f.HighestReceivedBlockSlot())
|
||||
require.Equal(t, primitives.Slot(0), f.HighestReceivedBlockDelay())
|
||||
|
||||
// 64
|
||||
// Received block last epoch is 1
|
||||
@@ -337,6 +352,7 @@ func TestForkChoice_ReceivedBlocksLastEpoch(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), count)
|
||||
require.Equal(t, primitives.Slot(64), f.HighestReceivedBlockSlot())
|
||||
require.Equal(t, primitives.Slot(0), f.HighestReceivedBlockDelay())
|
||||
|
||||
// 64 65
|
||||
// Received block last epoch is 2
|
||||
@@ -349,6 +365,7 @@ func TestForkChoice_ReceivedBlocksLastEpoch(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(2), count)
|
||||
require.Equal(t, primitives.Slot(65), f.HighestReceivedBlockSlot())
|
||||
require.Equal(t, primitives.Slot(1), f.HighestReceivedBlockDelay())
|
||||
|
||||
// 64 65 66
|
||||
// Received block last epoch is 3
|
||||
@@ -700,3 +717,17 @@ func TestStore_CleanupInserting(t *testing.T) {
|
||||
require.NotNil(t, f.InsertNode(ctx, st, blk))
|
||||
require.Equal(t, false, f.HasNode(blk.Root()))
|
||||
}
|
||||
|
||||
func TestStore_HighestReceivedBlockDelay(t *testing.T) {
|
||||
f := ForkChoice{
|
||||
store: &Store{
|
||||
genesisTime: time.Unix(0, 0),
|
||||
highestReceivedNode: &Node{
|
||||
slot: 10,
|
||||
timestamp: time.Unix(int64(((10 + 12) * params.BeaconConfig().SecondsPerSlot)), 0), // 12 slots late
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
require.Equal(t, primitives.Slot(12), f.HighestReceivedBlockDelay())
|
||||
}
|
||||
|
||||
@@ -21,26 +21,24 @@ type ForkChoice struct {
|
||||
balancesByRoot forkchoice.BalancesByRooter // handler to obtain balances for the state with a given root
|
||||
}
|
||||
|
||||
var _ forkchoice.ForkChoicer = (*ForkChoice)(nil)
|
||||
|
||||
// Store defines the fork choice store which includes block nodes and the last view of checkpoint information.
|
||||
type Store struct {
|
||||
justifiedCheckpoint *forkchoicetypes.Checkpoint // latest justified epoch in store.
|
||||
unrealizedJustifiedCheckpoint *forkchoicetypes.Checkpoint // best unrealized justified checkpoint in store.
|
||||
unrealizedFinalizedCheckpoint *forkchoicetypes.Checkpoint // best unrealized finalized checkpoint in store.
|
||||
prevJustifiedCheckpoint *forkchoicetypes.Checkpoint // previous justified checkpoint in store.
|
||||
finalizedCheckpoint *forkchoicetypes.Checkpoint // latest finalized epoch in store.
|
||||
proposerBoostRoot [fieldparams.RootLength]byte // latest block root that was boosted after being received in a timely manner.
|
||||
previousProposerBoostRoot [fieldparams.RootLength]byte // previous block root that was boosted after being received in a timely manner.
|
||||
previousProposerBoostScore uint64 // previous proposer boosted root score.
|
||||
finalizedDependentRoot [fieldparams.RootLength]byte // dependent root at finalized checkpoint.
|
||||
committeeWeight uint64 // tracks the total active validator balance divided by the number of slots per Epoch.
|
||||
treeRootNode *Node // the root node of the store tree.
|
||||
headNode *Node // last head Node
|
||||
emptyNodeByRoot map[[fieldparams.RootLength]byte]*PayloadNode // nodes indexed by roots.
|
||||
fullNodeByRoot map[[fieldparams.RootLength]byte]*PayloadNode // full nodes (the payload was present) indexed by beacon block root.
|
||||
slashedIndices map[primitives.ValidatorIndex]bool // the list of equivocating validator indices
|
||||
originRoot [fieldparams.RootLength]byte // The genesis block root
|
||||
justifiedCheckpoint *forkchoicetypes.Checkpoint // latest justified epoch in store.
|
||||
unrealizedJustifiedCheckpoint *forkchoicetypes.Checkpoint // best unrealized justified checkpoint in store.
|
||||
unrealizedFinalizedCheckpoint *forkchoicetypes.Checkpoint // best unrealized finalized checkpoint in store.
|
||||
prevJustifiedCheckpoint *forkchoicetypes.Checkpoint // previous justified checkpoint in store.
|
||||
finalizedCheckpoint *forkchoicetypes.Checkpoint // latest finalized epoch in store.
|
||||
proposerBoostRoot [fieldparams.RootLength]byte // latest block root that was boosted after being received in a timely manner.
|
||||
previousProposerBoostRoot [fieldparams.RootLength]byte // previous block root that was boosted after being received in a timely manner.
|
||||
previousProposerBoostScore uint64 // previous proposer boosted root score.
|
||||
finalizedDependentRoot [fieldparams.RootLength]byte // dependent root at finalized checkpoint.
|
||||
committeeWeight uint64 // tracks the total active validator balance divided by the number of slots per Epoch.
|
||||
treeRootNode *Node // the root node of the store tree.
|
||||
headNode *Node // last head Node
|
||||
nodeByRoot map[[fieldparams.RootLength]byte]*Node // nodes indexed by roots.
|
||||
nodeByPayload map[[fieldparams.RootLength]byte]*Node // nodes indexed by payload Hash
|
||||
slashedIndices map[primitives.ValidatorIndex]bool // the list of equivocating validator indices
|
||||
originRoot [fieldparams.RootLength]byte // The genesis block root
|
||||
genesisTime time.Time
|
||||
highestReceivedNode *Node // The highest slot node.
|
||||
receivedBlocksLastEpoch [fieldparams.SlotsPerEpoch]primitives.Slot // Using `highestReceivedSlot`. The slot of blocks received in the last epoch.
|
||||
@@ -52,28 +50,19 @@ type Store struct {
|
||||
type Node struct {
|
||||
slot primitives.Slot // slot of the block converted to the node.
|
||||
root [fieldparams.RootLength]byte // root of the block converted to the node.
|
||||
blockHash [fieldparams.RootLength]byte // payloadHash of the block converted to the node.
|
||||
parent *PayloadNode // parent index of this node.
|
||||
payloadHash [fieldparams.RootLength]byte // payloadHash of the block converted to the node.
|
||||
parent *Node // parent index of this node.
|
||||
target *Node // target checkpoint for
|
||||
bestDescendant *Node // bestDescendant node of this node.
|
||||
children []*Node // the list of direct children of this Node
|
||||
justifiedEpoch primitives.Epoch // justifiedEpoch of this node.
|
||||
unrealizedJustifiedEpoch primitives.Epoch // the epoch that would be justified if the block would be advanced to the next epoch.
|
||||
finalizedEpoch primitives.Epoch // finalizedEpoch of this node.
|
||||
unrealizedFinalizedEpoch primitives.Epoch // the epoch that would be finalized if the block would be advanced to the next epoch.
|
||||
balance uint64 // the balance that voted for this node directly
|
||||
weight uint64 // weight of this node: the total balance including children
|
||||
}
|
||||
|
||||
// PayloadNode defines a full Forkchoice node after the Gloas fork, with the payload status either empty of full
|
||||
type PayloadNode struct {
|
||||
optimistic bool // whether the block has been fully validated or not
|
||||
full bool // whether this node represents a payload present or not
|
||||
weight uint64 // weight of this node: the total balance including children
|
||||
balance uint64 // the balance that voted for this node directly
|
||||
bestDescendant *Node // bestDescendant node of this payload node.
|
||||
node *Node // the consensus part of this full forkchoice node
|
||||
timestamp time.Time // The timestamp when the node was inserted.
|
||||
children []*Node // the list of direct children of this Node
|
||||
bestDescendant *Node // bestDescendant node of this node.
|
||||
optimistic bool // whether the block has been fully validated or not
|
||||
timestamp time.Time // The timestamp when the node was inserted.
|
||||
}
|
||||
|
||||
// Vote defines an individual validator's vote.
|
||||
|
||||
@@ -15,34 +15,33 @@ import (
|
||||
)
|
||||
|
||||
func (s *Store) setUnrealizedJustifiedEpoch(root [32]byte, epoch primitives.Epoch) error {
|
||||
en, ok := s.emptyNodeByRoot[root]
|
||||
if !ok || en == nil {
|
||||
node, ok := s.nodeByRoot[root]
|
||||
if !ok || node == nil {
|
||||
return errors.Wrap(ErrNilNode, "could not set unrealized justified epoch")
|
||||
}
|
||||
if epoch < en.node.unrealizedJustifiedEpoch {
|
||||
if epoch < node.unrealizedJustifiedEpoch {
|
||||
return errInvalidUnrealizedJustifiedEpoch
|
||||
}
|
||||
en.node.unrealizedJustifiedEpoch = epoch
|
||||
node.unrealizedJustifiedEpoch = epoch
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) setUnrealizedFinalizedEpoch(root [32]byte, epoch primitives.Epoch) error {
|
||||
en, ok := s.emptyNodeByRoot[root]
|
||||
if !ok || en == nil {
|
||||
node, ok := s.nodeByRoot[root]
|
||||
if !ok || node == nil {
|
||||
return errors.Wrap(ErrNilNode, "could not set unrealized finalized epoch")
|
||||
}
|
||||
if epoch < en.node.unrealizedFinalizedEpoch {
|
||||
if epoch < node.unrealizedFinalizedEpoch {
|
||||
return errInvalidUnrealizedFinalizedEpoch
|
||||
}
|
||||
en.node.unrealizedFinalizedEpoch = epoch
|
||||
node.unrealizedFinalizedEpoch = epoch
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateUnrealizedCheckpoints "realizes" the unrealized justified and finalized
|
||||
// epochs stored within nodes. It should be called at the beginning of each epoch.
|
||||
func (f *ForkChoice) updateUnrealizedCheckpoints(ctx context.Context) error {
|
||||
for _, en := range f.store.emptyNodeByRoot {
|
||||
node := en.node
|
||||
for _, node := range f.store.nodeByRoot {
|
||||
node.justifiedEpoch = node.unrealizedJustifiedEpoch
|
||||
node.finalizedEpoch = node.unrealizedFinalizedEpoch
|
||||
if node.justifiedEpoch > f.store.justifiedCheckpoint.Epoch {
|
||||
@@ -63,17 +62,16 @@ func (s *Store) pullTips(state state.BeaconState, node *Node, jc, fc *ethpb.Chec
|
||||
if node.parent == nil { // Nothing to do if the parent is nil.
|
||||
return jc, fc
|
||||
}
|
||||
pn := node.parent.node
|
||||
currentEpoch := slots.ToEpoch(slots.CurrentSlot(s.genesisTime))
|
||||
stateSlot := state.Slot()
|
||||
stateEpoch := slots.ToEpoch(stateSlot)
|
||||
currJustified := pn.unrealizedJustifiedEpoch == currentEpoch
|
||||
prevJustified := pn.unrealizedJustifiedEpoch+1 == currentEpoch
|
||||
currJustified := node.parent.unrealizedJustifiedEpoch == currentEpoch
|
||||
prevJustified := node.parent.unrealizedJustifiedEpoch+1 == currentEpoch
|
||||
tooEarlyForCurr := slots.SinceEpochStarts(stateSlot)*3 < params.BeaconConfig().SlotsPerEpoch*2
|
||||
// Exit early if it's justified or too early to be justified.
|
||||
if currJustified || (stateEpoch == currentEpoch && prevJustified && tooEarlyForCurr) {
|
||||
node.unrealizedJustifiedEpoch = pn.unrealizedJustifiedEpoch
|
||||
node.unrealizedFinalizedEpoch = pn.unrealizedFinalizedEpoch
|
||||
node.unrealizedJustifiedEpoch = node.parent.unrealizedJustifiedEpoch
|
||||
node.unrealizedFinalizedEpoch = node.parent.unrealizedFinalizedEpoch
|
||||
return jc, fc
|
||||
}
|
||||
|
||||
|
||||
@@ -22,12 +22,12 @@ func TestStore_SetUnrealizedEpochs(t *testing.T) {
|
||||
state, blkRoot, err = prepareForkchoiceState(ctx, 102, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 1, 1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
require.Equal(t, primitives.Epoch(1), f.store.emptyNodeByRoot[[32]byte{'b'}].node.unrealizedJustifiedEpoch)
|
||||
require.Equal(t, primitives.Epoch(1), f.store.emptyNodeByRoot[[32]byte{'b'}].node.unrealizedFinalizedEpoch)
|
||||
require.Equal(t, primitives.Epoch(1), f.store.nodeByRoot[[32]byte{'b'}].unrealizedJustifiedEpoch)
|
||||
require.Equal(t, primitives.Epoch(1), f.store.nodeByRoot[[32]byte{'b'}].unrealizedFinalizedEpoch)
|
||||
require.NoError(t, f.store.setUnrealizedJustifiedEpoch([32]byte{'b'}, 2))
|
||||
require.NoError(t, f.store.setUnrealizedFinalizedEpoch([32]byte{'b'}, 2))
|
||||
require.Equal(t, primitives.Epoch(2), f.store.emptyNodeByRoot[[32]byte{'b'}].node.unrealizedJustifiedEpoch)
|
||||
require.Equal(t, primitives.Epoch(2), f.store.emptyNodeByRoot[[32]byte{'b'}].node.unrealizedFinalizedEpoch)
|
||||
require.Equal(t, primitives.Epoch(2), f.store.nodeByRoot[[32]byte{'b'}].unrealizedJustifiedEpoch)
|
||||
require.Equal(t, primitives.Epoch(2), f.store.nodeByRoot[[32]byte{'b'}].unrealizedFinalizedEpoch)
|
||||
|
||||
require.ErrorIs(t, errInvalidUnrealizedJustifiedEpoch, f.store.setUnrealizedJustifiedEpoch([32]byte{'b'}, 0))
|
||||
require.ErrorIs(t, errInvalidUnrealizedFinalizedEpoch, f.store.setUnrealizedFinalizedEpoch([32]byte{'b'}, 0))
|
||||
@@ -78,9 +78,9 @@ func TestStore_LongFork(t *testing.T) {
|
||||
// Add an attestation to c, it is head
|
||||
f.ProcessAttestation(ctx, []uint64{0}, [32]byte{'c'}, 1)
|
||||
f.justifiedBalances = []uint64{100}
|
||||
c := f.store.emptyNodeByRoot[[32]byte{'c'}]
|
||||
require.Equal(t, primitives.Epoch(2), slots.ToEpoch(c.node.slot))
|
||||
driftGenesisTime(f, c.node.slot, 0)
|
||||
c := f.store.nodeByRoot[[32]byte{'c'}]
|
||||
require.Equal(t, primitives.Epoch(2), slots.ToEpoch(c.slot))
|
||||
driftGenesisTime(f, c.slot, 0)
|
||||
headRoot, err := f.Head(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'c'}, headRoot)
|
||||
@@ -91,15 +91,15 @@ func TestStore_LongFork(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.InsertNode(ctx, state, blkRoot))
|
||||
require.NoError(t, f.UpdateJustifiedCheckpoint(ctx, &forkchoicetypes.Checkpoint{Epoch: 2, Root: ha}))
|
||||
d := f.store.emptyNodeByRoot[[32]byte{'d'}]
|
||||
require.Equal(t, primitives.Epoch(3), slots.ToEpoch(d.node.slot))
|
||||
driftGenesisTime(f, d.node.slot, 0)
|
||||
require.Equal(t, true, d.node.viableForHead(f.store.justifiedCheckpoint.Epoch, slots.ToEpoch(d.node.slot)))
|
||||
d := f.store.nodeByRoot[[32]byte{'d'}]
|
||||
require.Equal(t, primitives.Epoch(3), slots.ToEpoch(d.slot))
|
||||
driftGenesisTime(f, d.slot, 0)
|
||||
require.Equal(t, true, d.viableForHead(f.store.justifiedCheckpoint.Epoch, slots.ToEpoch(d.slot)))
|
||||
headRoot, err = f.Head(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'c'}, headRoot)
|
||||
require.Equal(t, uint64(0), f.store.emptyNodeByRoot[[32]byte{'d'}].weight)
|
||||
require.Equal(t, uint64(100), f.store.emptyNodeByRoot[[32]byte{'c'}].weight)
|
||||
require.Equal(t, uint64(0), f.store.nodeByRoot[[32]byte{'d'}].weight)
|
||||
require.Equal(t, uint64(100), f.store.nodeByRoot[[32]byte{'c'}].weight)
|
||||
}
|
||||
|
||||
// Epoch 1 Epoch 2 Epoch 3
|
||||
@@ -243,8 +243,8 @@ func TestStore_ForkNextEpoch(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'d'}, headRoot)
|
||||
require.Equal(t, primitives.Epoch(2), f.JustifiedCheckpoint().Epoch)
|
||||
require.Equal(t, uint64(0), f.store.emptyNodeByRoot[[32]byte{'d'}].weight)
|
||||
require.Equal(t, uint64(100), f.store.emptyNodeByRoot[[32]byte{'h'}].weight)
|
||||
require.Equal(t, uint64(0), f.store.nodeByRoot[[32]byte{'d'}].weight)
|
||||
require.Equal(t, uint64(100), f.store.nodeByRoot[[32]byte{'h'}].weight)
|
||||
// Set current epoch to 3, and H's unrealized checkpoint. Check it's head
|
||||
driftGenesisTime(f, 99, 0)
|
||||
require.NoError(t, f.store.setUnrealizedJustifiedEpoch([32]byte{'h'}, 2))
|
||||
@@ -252,8 +252,8 @@ func TestStore_ForkNextEpoch(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'h'}, headRoot)
|
||||
require.Equal(t, primitives.Epoch(2), f.JustifiedCheckpoint().Epoch)
|
||||
require.Equal(t, uint64(0), f.store.emptyNodeByRoot[[32]byte{'d'}].weight)
|
||||
require.Equal(t, uint64(100), f.store.emptyNodeByRoot[[32]byte{'h'}].weight)
|
||||
require.Equal(t, uint64(0), f.store.nodeByRoot[[32]byte{'d'}].weight)
|
||||
require.Equal(t, uint64(100), f.store.nodeByRoot[[32]byte{'h'}].weight)
|
||||
}
|
||||
|
||||
func TestStore_PullTips_Heuristics(t *testing.T) {
|
||||
@@ -263,14 +263,14 @@ func TestStore_PullTips_Heuristics(t *testing.T) {
|
||||
st, root, err := prepareForkchoiceState(ctx, 65, [32]byte{'p'}, [32]byte{}, [32]byte{}, 1, 1)
|
||||
require.NoError(tt, err)
|
||||
require.NoError(tt, f.InsertNode(ctx, st, root))
|
||||
f.store.emptyNodeByRoot[[32]byte{'p'}].node.unrealizedJustifiedEpoch = primitives.Epoch(2)
|
||||
f.store.nodeByRoot[[32]byte{'p'}].unrealizedJustifiedEpoch = primitives.Epoch(2)
|
||||
driftGenesisTime(f, 66, 0)
|
||||
|
||||
st, root, err = prepareForkchoiceState(ctx, 66, [32]byte{'h'}, [32]byte{'p'}, [32]byte{}, 1, 1)
|
||||
require.NoError(tt, err)
|
||||
require.NoError(tt, f.InsertNode(ctx, st, root))
|
||||
require.Equal(tt, primitives.Epoch(2), f.store.emptyNodeByRoot[[32]byte{'h'}].node.unrealizedJustifiedEpoch)
|
||||
require.Equal(tt, primitives.Epoch(1), f.store.emptyNodeByRoot[[32]byte{'h'}].node.unrealizedFinalizedEpoch)
|
||||
require.Equal(tt, primitives.Epoch(2), f.store.nodeByRoot[[32]byte{'h'}].unrealizedJustifiedEpoch)
|
||||
require.Equal(tt, primitives.Epoch(1), f.store.nodeByRoot[[32]byte{'h'}].unrealizedFinalizedEpoch)
|
||||
})
|
||||
|
||||
t.Run("Previous Epoch is justified and too early for current", func(tt *testing.T) {
|
||||
@@ -278,21 +278,21 @@ func TestStore_PullTips_Heuristics(t *testing.T) {
|
||||
st, root, err := prepareForkchoiceState(ctx, 95, [32]byte{'p'}, [32]byte{}, [32]byte{}, 1, 1)
|
||||
require.NoError(tt, err)
|
||||
require.NoError(tt, f.InsertNode(ctx, st, root))
|
||||
f.store.emptyNodeByRoot[[32]byte{'p'}].node.unrealizedJustifiedEpoch = primitives.Epoch(2)
|
||||
f.store.nodeByRoot[[32]byte{'p'}].unrealizedJustifiedEpoch = primitives.Epoch(2)
|
||||
driftGenesisTime(f, 96, 0)
|
||||
|
||||
st, root, err = prepareForkchoiceState(ctx, 96, [32]byte{'h'}, [32]byte{'p'}, [32]byte{}, 1, 1)
|
||||
require.NoError(tt, err)
|
||||
require.NoError(tt, f.InsertNode(ctx, st, root))
|
||||
require.Equal(tt, primitives.Epoch(2), f.store.emptyNodeByRoot[[32]byte{'h'}].node.unrealizedJustifiedEpoch)
|
||||
require.Equal(tt, primitives.Epoch(1), f.store.emptyNodeByRoot[[32]byte{'h'}].node.unrealizedFinalizedEpoch)
|
||||
require.Equal(tt, primitives.Epoch(2), f.store.nodeByRoot[[32]byte{'h'}].unrealizedJustifiedEpoch)
|
||||
require.Equal(tt, primitives.Epoch(1), f.store.nodeByRoot[[32]byte{'h'}].unrealizedFinalizedEpoch)
|
||||
})
|
||||
t.Run("Previous Epoch is justified and not too early for current", func(tt *testing.T) {
|
||||
f := setup(1, 1)
|
||||
st, root, err := prepareForkchoiceState(ctx, 95, [32]byte{'p'}, [32]byte{}, [32]byte{}, 1, 1)
|
||||
require.NoError(tt, err)
|
||||
require.NoError(tt, f.InsertNode(ctx, st, root))
|
||||
f.store.emptyNodeByRoot[[32]byte{'p'}].node.unrealizedJustifiedEpoch = primitives.Epoch(2)
|
||||
f.store.nodeByRoot[[32]byte{'p'}].unrealizedJustifiedEpoch = primitives.Epoch(2)
|
||||
driftGenesisTime(f, 127, 0)
|
||||
|
||||
st, root, err = prepareForkchoiceState(ctx, 127, [32]byte{'h'}, [32]byte{'p'}, [32]byte{}, 1, 1)
|
||||
@@ -302,14 +302,14 @@ func TestStore_PullTips_Heuristics(t *testing.T) {
|
||||
// This test checks that the heuristics in pullTips did not apply and
|
||||
// the test continues to compute a bogus unrealized
|
||||
// justification
|
||||
require.Equal(tt, primitives.Epoch(1), f.store.emptyNodeByRoot[[32]byte{'h'}].node.unrealizedJustifiedEpoch)
|
||||
require.Equal(tt, primitives.Epoch(1), f.store.nodeByRoot[[32]byte{'h'}].unrealizedJustifiedEpoch)
|
||||
})
|
||||
t.Run("Block from previous Epoch", func(tt *testing.T) {
|
||||
f := setup(1, 1)
|
||||
st, root, err := prepareForkchoiceState(ctx, 94, [32]byte{'p'}, [32]byte{}, [32]byte{}, 1, 1)
|
||||
require.NoError(tt, err)
|
||||
require.NoError(tt, f.InsertNode(ctx, st, root))
|
||||
f.store.emptyNodeByRoot[[32]byte{'p'}].node.unrealizedJustifiedEpoch = primitives.Epoch(2)
|
||||
f.store.nodeByRoot[[32]byte{'p'}].unrealizedJustifiedEpoch = primitives.Epoch(2)
|
||||
driftGenesisTime(f, 96, 0)
|
||||
|
||||
st, root, err = prepareForkchoiceState(ctx, 95, [32]byte{'h'}, [32]byte{'p'}, [32]byte{}, 1, 1)
|
||||
@@ -319,7 +319,7 @@ func TestStore_PullTips_Heuristics(t *testing.T) {
|
||||
// This test checks that the heuristics in pullTips did not apply and
|
||||
// the test continues to compute a bogus unrealized
|
||||
// justification
|
||||
require.Equal(tt, primitives.Epoch(1), f.store.emptyNodeByRoot[[32]byte{'h'}].node.unrealizedJustifiedEpoch)
|
||||
require.Equal(tt, primitives.Epoch(1), f.store.nodeByRoot[[32]byte{'h'}].unrealizedJustifiedEpoch)
|
||||
})
|
||||
t.Run("Previous Epoch is not justified", func(tt *testing.T) {
|
||||
f := setup(1, 1)
|
||||
@@ -335,6 +335,6 @@ func TestStore_PullTips_Heuristics(t *testing.T) {
|
||||
// This test checks that the heuristics in pullTips did not apply and
|
||||
// the test continues to compute a bogus unrealized
|
||||
// justification
|
||||
require.Equal(tt, primitives.Epoch(2), f.store.emptyNodeByRoot[[32]byte{'h'}].node.unrealizedJustifiedEpoch)
|
||||
require.Equal(tt, primitives.Epoch(2), f.store.nodeByRoot[[32]byte{'h'}].unrealizedJustifiedEpoch)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ func TestVotes_CanFindHead(t *testing.T) {
|
||||
// 9 10
|
||||
f.store.finalizedCheckpoint.Root = indexToHash(5)
|
||||
require.NoError(t, f.store.prune(t.Context()))
|
||||
assert.Equal(t, 5, len(f.store.emptyNodeByRoot), "Incorrect nodes length after prune")
|
||||
assert.Equal(t, 5, len(f.store.nodeByRoot), "Incorrect nodes length after prune")
|
||||
// we pruned artificially the justified root.
|
||||
f.store.justifiedCheckpoint.Root = indexToHash(5)
|
||||
|
||||
|
||||
@@ -67,11 +67,13 @@ type FastGetter interface {
|
||||
HasNode([32]byte) bool
|
||||
HighestReceivedBlockSlot() primitives.Slot
|
||||
HighestReceivedBlockRoot() [32]byte
|
||||
HighestReceivedBlockDelay() primitives.Slot
|
||||
IsCanonical(root [32]byte) bool
|
||||
IsOptimistic(root [32]byte) (bool, error)
|
||||
IsViableForCheckpoint(*forkchoicetypes.Checkpoint) (bool, error)
|
||||
JustifiedCheckpoint() *forkchoicetypes.Checkpoint
|
||||
JustifiedPayloadBlockHash() [32]byte
|
||||
LastRoot(primitives.Epoch) [32]byte
|
||||
NodeCount() int
|
||||
PreviousJustifiedCheckpoint() *forkchoicetypes.Checkpoint
|
||||
ProposerBoost() [fieldparams.RootLength]byte
|
||||
@@ -89,7 +91,7 @@ type FastGetter interface {
|
||||
// Setter allows to set forkchoice information
|
||||
type Setter interface {
|
||||
SetOptimisticToValid(context.Context, [fieldparams.RootLength]byte) error
|
||||
SetOptimisticToInvalid(context.Context, [32]byte, [32]byte, [32]byte, [32]byte) ([][32]byte, error)
|
||||
SetOptimisticToInvalid(context.Context, [fieldparams.RootLength]byte, [fieldparams.RootLength]byte, [fieldparams.RootLength]byte) ([][32]byte, error)
|
||||
UpdateJustifiedCheckpoint(context.Context, *forkchoicetypes.Checkpoint) error
|
||||
UpdateFinalizedCheckpoint(*forkchoicetypes.Checkpoint) error
|
||||
SetGenesisTime(time.Time)
|
||||
|
||||
@@ -121,6 +121,13 @@ func (ro *ROForkChoice) HighestReceivedBlockRoot() [32]byte {
|
||||
return ro.getter.HighestReceivedBlockRoot()
|
||||
}
|
||||
|
||||
// HighestReceivedBlockDelay delegates to the underlying forkchoice call, under a lock.
|
||||
func (ro *ROForkChoice) HighestReceivedBlockDelay() primitives.Slot {
|
||||
ro.l.RLock()
|
||||
defer ro.l.RUnlock()
|
||||
return ro.getter.HighestReceivedBlockDelay()
|
||||
}
|
||||
|
||||
// ReceivedBlocksLastEpoch delegates to the underlying forkchoice call, under a lock.
|
||||
func (ro *ROForkChoice) ReceivedBlocksLastEpoch() (uint64, error) {
|
||||
ro.l.RLock()
|
||||
@@ -156,6 +163,13 @@ func (ro *ROForkChoice) Slot(root [32]byte) (primitives.Slot, error) {
|
||||
return ro.getter.Slot(root)
|
||||
}
|
||||
|
||||
// LastRoot delegates to the underlying forkchoice call, under a lock.
|
||||
func (ro *ROForkChoice) LastRoot(e primitives.Epoch) [32]byte {
|
||||
ro.l.RLock()
|
||||
defer ro.l.RUnlock()
|
||||
return ro.getter.LastRoot(e)
|
||||
}
|
||||
|
||||
// DependentRoot delegates to the underlying forkchoice call, under a lock.
|
||||
func (ro *ROForkChoice) DependentRoot(epoch primitives.Epoch) ([32]byte, error) {
|
||||
ro.l.RLock()
|
||||
|
||||
@@ -30,6 +30,7 @@ const (
|
||||
nodeCountCalled
|
||||
highestReceivedBlockSlotCalled
|
||||
highestReceivedBlockRootCalled
|
||||
highestReceivedBlockDelayCalled
|
||||
receivedBlocksLastEpochCalled
|
||||
weightCalled
|
||||
isOptimisticCalled
|
||||
@@ -117,6 +118,11 @@ func TestROLocking(t *testing.T) {
|
||||
call: highestReceivedBlockSlotCalled,
|
||||
cb: func(g FastGetter) { g.HighestReceivedBlockSlot() },
|
||||
},
|
||||
{
|
||||
name: "highestReceivedBlockDelayCalled",
|
||||
call: highestReceivedBlockDelayCalled,
|
||||
cb: func(g FastGetter) { g.HighestReceivedBlockDelay() },
|
||||
},
|
||||
{
|
||||
name: "receivedBlocksLastEpochCalled",
|
||||
call: receivedBlocksLastEpochCalled,
|
||||
@@ -142,6 +148,11 @@ func TestROLocking(t *testing.T) {
|
||||
call: slotCalled,
|
||||
cb: func(g FastGetter) { _, err := g.Slot([32]byte{}); _discard(t, err) },
|
||||
},
|
||||
{
|
||||
name: "lastRootCalled",
|
||||
call: lastRootCalled,
|
||||
cb: func(g FastGetter) { g.LastRoot(0) },
|
||||
},
|
||||
{
|
||||
name: "targetRootForEpochCalled",
|
||||
call: targetRootForEpochCalled,
|
||||
@@ -254,6 +265,11 @@ func (ro *mockROForkchoice) HighestReceivedBlockRoot() [32]byte {
|
||||
return [32]byte{}
|
||||
}
|
||||
|
||||
func (ro *mockROForkchoice) HighestReceivedBlockDelay() primitives.Slot {
|
||||
ro.calls = append(ro.calls, highestReceivedBlockDelayCalled)
|
||||
return 0
|
||||
}
|
||||
|
||||
func (ro *mockROForkchoice) ReceivedBlocksLastEpoch() (uint64, error) {
|
||||
ro.calls = append(ro.calls, receivedBlocksLastEpochCalled)
|
||||
return 0, nil
|
||||
@@ -279,6 +295,11 @@ func (ro *mockROForkchoice) Slot(_ [32]byte) (primitives.Slot, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (ro *mockROForkchoice) LastRoot(_ primitives.Epoch) [32]byte {
|
||||
ro.calls = append(ro.calls, lastRootCalled)
|
||||
return [32]byte{}
|
||||
}
|
||||
|
||||
// DependentRoot impoements FastGetter.
|
||||
func (ro *mockROForkchoice) DependentRoot(_ primitives.Epoch) ([32]byte, error) {
|
||||
ro.calls = append(ro.calls, dependentRootCalled)
|
||||
|
||||
95
beacon-chain/graffiti/graffiti-proposal-brief.md
Normal file
95
beacon-chain/graffiti/graffiti-proposal-brief.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Graffiti Version Info Implementation
|
||||
|
||||
## Summary
|
||||
Add automatic EL+CL version info to block graffiti following [ethereum/execution-apis#517](https://github.com/ethereum/execution-apis/pull/517). Uses the [flexible standard](https://hackmd.io/@wmoBhF17RAOH2NZ5bNXJVg/BJX2c9gja) to pack client info into leftover space after user graffiti.
|
||||
|
||||
More details: https://github.com/ethereum/execution-apis/blob/main/src/engine/identification.md
|
||||
|
||||
## Implementation
|
||||
|
||||
### Core Component: GraffitiInfo Struct
|
||||
Thread-safe struct holding version information:
|
||||
```go
|
||||
const clCode = "PR"
|
||||
|
||||
type GraffitiInfo struct {
|
||||
mu sync.RWMutex
|
||||
userGraffiti string // From --graffiti flag (set once at startup)
|
||||
clCommit string // From version.GetCommitPrefix() helper function
|
||||
elCode string // From engine_getClientVersionV1
|
||||
elCommit string // From engine_getClientVersionV1
|
||||
}
|
||||
```
|
||||
|
||||
### Flow
|
||||
1. **Startup**: Parse flags, create GraffitiInfo with user graffiti and CL info.
|
||||
2. **Wiring**: Pass struct to both execution service and RPC validator server
|
||||
3. **Runtime**: Execution service goroutine periodically calls `engine_getClientVersionV1` and updates EL fields
|
||||
4. **Block Proposal**: RPC validator server calls `GenerateGraffiti()` to get formatted graffiti
|
||||
|
||||
### Flexible Graffiti Format
|
||||
Packs as much client info as space allows (after user graffiti):
|
||||
|
||||
| Available Space | Format | Example |
|
||||
|----------------|--------|---------|
|
||||
| ≥12 bytes | `EL(2)+commit(4)+CL(2)+commit(4)+user` | `GE168dPR63afBob` |
|
||||
| 8-11 bytes | `EL(2)+commit(2)+CL(2)+commit(2)+user` | `GE16PR63my node here` |
|
||||
| 4-7 bytes | `EL(2)+CL(2)+user` | `GEPRthis is my graffiti msg` |
|
||||
| 2-3 bytes | `EL(2)+user` | `GEalmost full graffiti message` |
|
||||
| <2 bytes | user only | `full 32 byte user graffiti here` |
|
||||
|
||||
```go
|
||||
func (g *GraffitiInfo) GenerateGraffiti() [32]byte {
|
||||
available := 32 - len(userGraffiti)
|
||||
|
||||
if elCode == "" {
|
||||
elCommit2 = elCommit4 = ""
|
||||
}
|
||||
|
||||
switch {
|
||||
case available >= 12:
|
||||
return elCode + elCommit4 + clCode + clCommit4 + userGraffiti
|
||||
case available >= 8:
|
||||
return elCode + elCommit2 + clCode + clCommit2 + userGraffiti
|
||||
case available >= 4:
|
||||
return elCode + clCode + userGraffiti
|
||||
case available >= 2:
|
||||
return elCode + userGraffiti
|
||||
default:
|
||||
return userGraffiti
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Update Logic
|
||||
Single testable function in execution service:
|
||||
```go
|
||||
func (s *Service) updateGraffitiInfo() {
|
||||
versions, err := s.GetClientVersion(ctx)
|
||||
if err != nil {
|
||||
return // Keep last good value
|
||||
}
|
||||
if len(versions) == 1 {
|
||||
s.graffitiInfo.UpdateFromEngine(versions[0].Code, versions[0].Commit)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Goroutine calls this on `slot % 8 == 4` timing (4 times per epoch, avoids slot boundaries).
|
||||
|
||||
### Files Changes Required
|
||||
|
||||
**New:**
|
||||
- `beacon-chain/execution/graffiti_info.go` - The struct and methods
|
||||
- `beacon-chain/execution/graffiti_info_test.go` - Unit tests
|
||||
- `runtime/version/version.go` - Add `GetCommitPrefix()` helper that extracts first 4 hex chars from the git commit injected via Bazel ldflags at build time
|
||||
|
||||
**Modified:**
|
||||
- `beacon-chain/execution/service.go` - Add goroutine + updateGraffitiInfo()
|
||||
- `beacon-chain/execution/engine_client.go` - Add GetClientVersion() method that does engine call
|
||||
- `beacon-chain/rpc/.../validator/proposer.go` - Call GenerateGraffiti()
|
||||
- `beacon-chain/node/node.go` - Wire GraffitiInfo to services
|
||||
|
||||
### Testing Strategy
|
||||
- Unit test GraffitiInfo methods (priority logic, thread safety)
|
||||
- Unit test updateGraffitiInfo() with mocked engine client
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user