Skip to content

Commit

Permalink
beacon/light/api, cmd/blsync: removed old state proof format and beac…
Browse files Browse the repository at this point in the history
…on.api.stateproof flag
  • Loading branch information
zsfelfoldi committed Mar 15, 2023
1 parent d308c70 commit 9a8301e
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 210 deletions.
139 changes: 15 additions & 124 deletions beacon/light/api/light_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package api

import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -49,20 +48,18 @@ var (
// BeaconLightApi requests light client information from a beacon node REST API.
// Note: all required API endpoints are currently only implemented by Lodestar.
type BeaconLightApi struct {
url string
client *http.Client
customHeaders map[string]string
stateProofVersion int
url string
client *http.Client
customHeaders map[string]string
}

func NewBeaconLightApi(url string, customHeaders map[string]string, stateProofVersion int) *BeaconLightApi {
func NewBeaconLightApi(url string, customHeaders map[string]string) *BeaconLightApi {
return &BeaconLightApi{
url: url,
client: &http.Client{
Timeout: time.Second * 10,
},
customHeaders: customHeaders,
stateProofVersion: stateProofVersion,
customHeaders: customHeaders,
}
}

Expand Down Expand Up @@ -214,63 +211,40 @@ func (api *BeaconLightApi) GetHeader(blockRoot common.Hash) (types.Header, error
}

// does not verify state root
func (api *BeaconLightApi) GetHeadStateProof(format merkle.ProofFormat, paths []string) (merkle.MultiProof, error) {
if api.stateProofVersion >= 2 {
encFormat, bitLength := EncodeCompactProofFormat(format)
return api.getStateProof("head", format, encFormat, bitLength)
} else {
proof, _, err := api.getOldStateProof("head", format, paths)
return proof, err
}
func (api *BeaconLightApi) GetHeadStateProof(format merkle.ProofFormat) (merkle.MultiProof, error) {
encFormat, bitLength := EncodeCompactProofFormat(format)
return api.getStateProof("head", format, encFormat, bitLength)
}

type StateProofSub struct {
api *BeaconLightApi
format merkle.ProofFormat
paths []string
encFormat []byte
bitLength int
}

func (api *BeaconLightApi) SubscribeStateProof(format merkle.ProofFormat, paths []string, first, period int) (*StateProofSub, error) {
if api.stateProofVersion == 0 {
return nil, errors.New("State proof API disabled")
}
func (api *BeaconLightApi) SubscribeStateProof(format merkle.ProofFormat, first, period int) (*StateProofSub, error) {
encFormat, bitLength := EncodeCompactProofFormat(format)
if api.stateProofVersion >= 2 {
_, err := api.httpGetf("/eth/v0/beacon/proof/subscribe/states?format=0x%x&first=%d&period=%d", encFormat, first, period)
if err != nil && err != ErrNotFound {
return nil, err
}
_, err := api.httpGetf("/eth/v0/beacon/proof/subscribe/states?format=0x%x&first=%d&period=%d", encFormat, first, period)
if err != nil && err != ErrNotFound {
// if subscribe endpoint is missing then we expect proof endpoint to serve recent states without subscription
return nil, err
}
return &StateProofSub{
api: api,
format: format,
encFormat: encFormat,
bitLength: bitLength,
paths: paths,
}, nil
}

// verifies state root
func (sub *StateProofSub) Get(stateRoot common.Hash) (merkle.MultiProof, error) {
var (
proof merkle.MultiProof
rootHash common.Hash
err error
)
if sub.api.stateProofVersion >= 2 {
proof, err = sub.api.getStateProof(stateRoot.Hex(), sub.format, sub.encFormat, sub.bitLength)
if err == nil {
rootHash = proof.RootHash()
}
} else {
proof, rootHash, err = sub.api.getOldStateProof(stateRoot.Hex(), sub.format, sub.paths)
}
proof, err := sub.api.getStateProof(stateRoot.Hex(), sub.format, sub.encFormat, sub.bitLength)
if err != nil {
return merkle.MultiProof{}, err
}
if rootHash != stateRoot {
if proof.RootHash() != stateRoot {
return merkle.MultiProof{}, errors.New("Received proof has incorrect state root")
}
return proof, nil
Expand All @@ -292,89 +266,6 @@ func (api *BeaconLightApi) getStateProof(stateId string, format merkle.ProofForm
return merkle.MultiProof{Format: format, Values: values}, nil
}

// getOldStateProof fetches and validates a Merkle proof for the specified parts of
// the recent beacon state referenced by stateRoot. If successful the returned
// multiproof has the format specified by expFormat. The state subset specified by
// the list of string keys (paths) should cover the subset specified by expFormat.
func (api *BeaconLightApi) getOldStateProof(stateId string, expFormat merkle.ProofFormat, paths []string) (merkle.MultiProof, common.Hash, error) {
path := "/eth/v0/beacon/proof/state/" + stateId + "?paths=" + paths[0]
for i := 1; i < len(paths); i++ {
path += "&paths=" + paths[i]
}
resp, err := api.httpGet(path)
if err != nil {
return merkle.MultiProof{}, common.Hash{}, err
}
proof, err := parseSSZMultiProof(resp)
if err != nil {
return merkle.MultiProof{}, common.Hash{}, err
}
var values merkle.Values
reader := proof.Reader(nil)
root, ok := merkle.TraverseProof(reader, merkle.NewMultiProofWriter(expFormat, &values, nil))
if !ok || !reader.Finished() {
return merkle.MultiProof{}, common.Hash{}, errors.New("invalid state proof")
}
return merkle.MultiProof{Format: expFormat, Values: values}, root, nil
}

// parseSSZMultiProof creates a MultiProof from a serialized format:
//
// 1 byte: always 1
// 2 bytes: leafCount
// (leafCount-1) * 2 bytes: as the tree is traversed in depth-first, left-to-right
// order, the number of leaves on the left branch of each traversed non-leaf
// subtree are listed here
// leafCount * 32 bytes: leaf values and internal sibling hashes in the same traversal order
//
// Note: this is the format generated by the /eth/v1/beacon/light_client/proof/
// beacon light client API endpoint which is currently only supported by Lodestar.
// A different format is proposed to be standardized so this function will
// probably be replaced later, see here:
//
// https://github.com/ethereum/consensus-specs/pull/3148
// https://github.com/ethereum/beacon-APIs/pull/267
func parseSSZMultiProof(proof []byte) (merkle.MultiProof, error) {
if len(proof) < 3 || proof[0] != 1 {
return merkle.MultiProof{}, errors.New("invalid proof length")
}
var (
leafCount = int(binary.LittleEndian.Uint16(proof[1:3]))
format = merkle.NewIndexMapFormat()
values = make(merkle.Values, leafCount)
valuesStart = leafCount*2 + 1
)
if len(proof) != leafCount*34+1 {
return merkle.MultiProof{}, errors.New("invalid proof length")
}
if err := parseMultiProofFormat(format, 1, proof[3:valuesStart]); err != nil {
return merkle.MultiProof{}, err
}
for i := range values {
copy(values[i][:], proof[valuesStart+i*32:valuesStart+(i+1)*32])
}
return merkle.MultiProof{Format: format, Values: values}, nil
}

// parseMultiProofFormat recursively parses the SSZ serialized proof format
func parseMultiProofFormat(indexMap merkle.IndexMapFormat, index uint64, format []byte) error {
indexMap.AddLeaf(index, nil)
if len(format) == 0 {
return nil
}
boundary := int(binary.LittleEndian.Uint16(format[:2])) * 2
if boundary > len(format) {
return errors.New("invalid proof format")
}
if err := parseMultiProofFormat(indexMap, index*2, format[2:boundary]); err != nil {
return err
}
if err := parseMultiProofFormat(indexMap, index*2+1, format[boundary:]); err != nil {
return err
}
return nil
}

// EncodeCompactProofFormat encodes a merkle.ProofFormat into a binary compact
// proof format. See description here:
// https://github.com/ChainSafe/consensus-specs/blob/feat/multiproof/ssz/merkle-proofs.md#compact-multiproofs
Expand Down
69 changes: 26 additions & 43 deletions cmd/blsync/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ func main() {
utils.BlsyncApiFlag,
utils.BlsyncJWTSecretFlag,
utils.BlsyncTestFlag,
utils.BeaconApiStateProofFlag,
}
app.Action = blsync

Expand Down Expand Up @@ -99,14 +98,13 @@ func blsync(ctx *cli.Context) error {
}

var (
stateProofVersion = ctx.Int(utils.BeaconApiStateProofFlag.Name)
beaconApi = api.NewBeaconLightApi(ctx.String(utils.BeaconApiFlag.Name), customHeader, stateProofVersion)
db = memorydb.New()
threshold = ctx.Int(utils.BeaconThresholdFlag.Name)
committeeChain = light.NewCommitteeChain(db, chainConfig.Forks, threshold, !ctx.Bool(utils.BeaconNoFilterFlag.Name), light.BLSVerifier{}, &mclock.System{}, func() int64 { return time.Now().UnixNano() })
checkpointStore = light.NewCheckpointStore(db, committeeChain)
headTracker = light.NewHeadTracker(committeeChain)
scheduler = sync.NewScheduler()
beaconApi = api.NewBeaconLightApi(ctx.String(utils.BeaconApiFlag.Name), customHeader)
db = memorydb.New()
threshold = ctx.Int(utils.BeaconThresholdFlag.Name)
committeeChain = light.NewCommitteeChain(db, chainConfig.Forks, threshold, !ctx.Bool(utils.BeaconNoFilterFlag.Name), light.BLSVerifier{}, &mclock.System{}, func() int64 { return time.Now().UnixNano() })
checkpointStore = light.NewCheckpointStore(db, committeeChain)
headTracker = light.NewHeadTracker(committeeChain)
scheduler = sync.NewScheduler()
)
committeeChain.SetGenesisData(chainConfig.GenesisData)
scheduler.RegisterModule(sync.NewCheckpointInit(committeeChain, checkpointStore, chainConfig.Checkpoint))
Expand All @@ -118,20 +116,13 @@ func blsync(ctx *cli.Context) error {
if ctx.IsSet(utils.BlsyncApiFlag.Name) || ctx.IsSet(utils.BlsyncJWTSecretFlag.Name) {
utils.Fatalf("Target engine API/JWT secret specified in test mode")
}
if !ctx.IsSet(utils.BeaconApiStateProofFlag.Name) {
stateProofVersion = 1
}
if stateProofVersion == 0 {
utils.Fatalf("Cannot run test mode without state proof API enabled")
}
testSyncer := newTestSyncer(beaconApi, stateProofVersion)
testSyncer := newTestSyncer(beaconApi)
headTracker.Subscribe(threshold, testSyncer.newSignedHead)
} else {
syncer := &execSyncer{
api: beaconApi,
client: makeRPCClient(ctx),
execRootCache: lru.NewCache[common.Hash, common.Hash](1000),
useStateProofs: stateProofVersion != 0,
api: beaconApi,
client: makeRPCClient(ctx),
execRootCache: lru.NewCache[common.Hash, common.Hash](1000),
}
headTracker.Subscribe(threshold, syncer.newHead)
}
Expand Down Expand Up @@ -163,16 +154,10 @@ func callForkchoiceUpdatedV1(client *rpc.Client, headHash, finalizedHash common.
}

type execSyncer struct {
api *api.BeaconLightApi
sub *api.StateProofSub
useStateProofs bool
client *rpc.Client
execRootCache *lru.Cache[common.Hash, common.Hash] // beacon block root -> execution block root
}

var statePaths = []string{
"[\"finalizedCheckpoint\",\"root\"]",
"[\"latestExecutionPayloadHeader\",\"blockHash\"]",
api *api.BeaconLightApi
sub *api.StateProofSub
client *rpc.Client
execRootCache *lru.Cache[common.Hash, common.Hash] // beacon block root -> execution block root
}

// newHead fetches state proofs to determine the execution block root and calls
Expand All @@ -187,21 +172,17 @@ func (e *execSyncer) newHead(signedHead types.SignedHead) {
}
blockRoot := block.Hash()
var finalizedExecRoot common.Hash
if e.useStateProofs {
if e.sub == nil {
if sub, err := e.api.SubscribeStateProof(stateProofFormat, statePaths, 0, 1); err == nil {
log.Info("Successfully created beacon state subscription")
e.sub = sub
} else {
log.Error("Failed to create beacon state subscription", "error", err)
return
}
}
proof, err := e.sub.Get(head.StateRoot)
if err != nil {
log.Error("Error fetching state proof from beacon API", "error", err)
if e.sub == nil {
if sub, err := e.api.SubscribeStateProof(stateProofFormat, 0, 1); err == nil {
log.Info("Successfully created beacon state subscription")
e.sub = sub
} else {
log.Error("Failed to create beacon state subscription", "error", err)
return
}
}
proof, err := e.sub.Get(head.StateRoot)
if err == nil {
var (
execBlockRoot = common.Hash(proof.Values[execBlockIndex])
finalizedBeaconRoot = common.Hash(proof.Values[finalizedBlockIndex])
Expand All @@ -216,6 +197,8 @@ func (e *execSyncer) newHead(signedHead types.SignedHead) {
e.fetchExecRoots(head.ParentRoot)
}
finalizedExecRoot, _ = e.execRootCache.Get(finalizedBeaconRoot)
} else if err != api.ErrNotFound {
log.Error("Error fetching state proof from beacon API", "error", err)
}
if e.client == nil { // dry run, no engine API specified
log.Info("New execution block retrieved", "block number", block.NumberU64(), "block hash", blockRoot, "finalized block hash", finalizedExecRoot)
Expand Down
Loading

0 comments on commit 9a8301e

Please sign in to comment.