Skip to content

Commit

Permalink
ethclient: fix BlobSidecars api
Browse files Browse the repository at this point in the history
  • Loading branch information
jhgdike committed Aug 20, 2024
1 parent 3924b9d commit bc5d39d
Show file tree
Hide file tree
Showing 6 changed files with 443 additions and 0 deletions.
100 changes: 100 additions & 0 deletions beacon/server_for_op_stack/api_func.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package server_for_op_stack

import (
"context"

Check failure on line 4 in beacon/server_for_op_stack/api_func.go

View workflow job for this annotation

GitHub Actions / golang-lint (1.21.x, ubuntu-latest)

File is not `goimports`-ed (goimports)
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
"sort"
)

var (
client *ethclient.Client
)

func Init(nodeURL string) {
var err error
client, err = ethclient.Dial(nodeURL)
if err != nil {
log.Crit("Error connecting to client", "nodeURL", nodeURL, "error", err)
}
}

type BlobSidecar struct {
Blob kzg4844.Blob `json:"blob"`
Index int `json:"index"`
KZGCommitment kzg4844.Commitment `json:"kzg_commitment"`
KZGProof kzg4844.Proof `json:"kzg_proof"`
}

type APIGetBlobSidecarsResponse struct {
Data []*BlobSidecar `json:"data"`
}

type ReducedGenesisData struct {
GenesisTime string `json:"genesis_time"`
}

type APIGenesisResponse struct {
Data ReducedGenesisData `json:"data"`
}

type ReducedConfigData struct {
SecondsPerSlot string `json:"SECONDS_PER_SLOT"`
}

type IndexedBlobHash struct {
Index int // absolute index in the block, a.k.a. position in sidecar blobs array
Hash common.Hash // hash of the blob, used for consistency checks
}

func configSpec() ReducedConfigData {
return ReducedConfigData{SecondsPerSlot: "1"}
}

func beaconGenesis() APIGenesisResponse {
return APIGenesisResponse{Data: ReducedGenesisData{GenesisTime: "0"}}
}

func beaconBlobSidecars(ctx context.Context, slot uint64, indices []int) (APIGetBlobSidecarsResponse, error) {
var blockNrOrHash rpc.BlockNumberOrHash
blockNum, err := fetchBlockNumberByTime(ctx, int64(slot), client)
if err != nil {
log.Error("Error fetching block number", "slot", slot, "indices", indices)
return APIGetBlobSidecarsResponse{}, err
}
rpcBlockNum := rpc.BlockNumber(blockNum)
blockNrOrHash.BlockNumber = &rpcBlockNum
sideCars, err := client.BlobSidecars(ctx, blockNrOrHash)
if err != nil {
log.Error("Error fetching Sidecars", "blockNrOrHash", blockNrOrHash, "err", err)
return APIGetBlobSidecarsResponse{}, err
}
sort.Ints(indices)
fullBlob := len(indices) == 0
res := APIGetBlobSidecarsResponse{}
idx := 0
curIdx := 0
for _, sideCar := range sideCars {
for i := 0; i < len(sideCar.Blobs); i++ {
//hash := kZGToVersionedHash(sideCar.Commitments[i])
if !fullBlob && curIdx >= len(indices) {
break
}
if fullBlob || idx == indices[curIdx] {
res.Data = append(res.Data, &BlobSidecar{
Index: idx,
Blob: sideCar.Blobs[i],
KZGCommitment: sideCar.Commitments[i],
KZGProof: sideCar.Proofs[i],
})
curIdx++
}
idx++
}
}

return res, nil
}
87 changes: 87 additions & 0 deletions beacon/server_for_op_stack/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package server_for_op_stack

import (
"fmt"
"github.com/prysmaticlabs/prysm/v5/api/server/structs"

Check failure on line 5 in beacon/server_for_op_stack/handlers.go

View workflow job for this annotation

GitHub Actions / golang-lint (1.21.x, ubuntu-latest)

File is not `goimports`-ed (goimports)
field_params "github.com/prysmaticlabs/prysm/v5/config/fieldparams"
"github.com/prysmaticlabs/prysm/v5/network/httputil"
"net/http"
"net/url"
"strconv"
"strings"
)

var (
versionMethod = "/eth/v1/node/version"
specMethod = "/eth/v1/config/spec"
genesisMethod = "/eth/v1/beacon/genesis"
sidecarsMethodPrefix = "/eth/v1/beacon/blob_sidecars/{slot}"
)

func VersionMethod(w http.ResponseWriter, r *http.Request) {
resp := &structs.GetVersionResponse{
Data: &structs.Version{
Version: "",
},
}
httputil.WriteJson(w, resp)
}

func SpecMethod(w http.ResponseWriter, r *http.Request) {
httputil.WriteJson(w, &structs.GetSpecResponse{Data: configSpec()})
}

func GenesisMethod(w http.ResponseWriter, r *http.Request) {
httputil.WriteJson(w, beaconGenesis())
}

func SidecarsMethod(w http.ResponseWriter, r *http.Request) {
indices, err := parseIndices(r.URL)
if err != nil {
httputil.HandleError(w, err.Error(), http.StatusBadRequest)
return
}
segments := strings.Split(r.URL.Path, "/")
slot, err := strconv.ParseUint(segments[len(segments)-1], 10, 64)
if err != nil {
httputil.HandleError(w, "not a valid slot(timestamp)", http.StatusBadRequest)
return
}

resp, err := beaconBlobSidecars(r.Context(), slot, indices)
if err != nil {
httputil.HandleError(w, err.Error(), http.StatusBadRequest)
return
}
httputil.WriteJson(w, resp)
}

// parseIndices filters out invalid and duplicate blob indices
func parseIndices(url *url.URL) ([]int, error) {
rawIndices := url.Query()["indices"]
indices := make([]int, 0, field_params.MaxBlobsPerBlock)
invalidIndices := make([]string, 0)
loop:
for _, raw := range rawIndices {
ix, err := strconv.Atoi(raw)
if err != nil {
invalidIndices = append(invalidIndices, raw)
continue
}
if ix >= field_params.MaxBlobsPerBlock {
invalidIndices = append(invalidIndices, raw)
continue
}
for i := range indices {
if ix == indices[i] {
continue loop
}
}
indices = append(indices, ix)
}

if len(invalidIndices) > 0 {
return nil, fmt.Errorf("requested blob indices %v are invalid", invalidIndices)
}
return indices, nil
}
73 changes: 73 additions & 0 deletions beacon/server_for_op_stack/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package server_for_op_stack

import (
"context"
"net/http"

"github.com/gorilla/mux"
"github.com/prysmaticlabs/prysm/v5/api/server"
)

type Config struct {
BSCNodeURL string
HostPort string
}

type Service struct {
cfg Config
router *mux.Router
}

func NewService(ctx context.Context, cfg *Config) *Service {
Init(cfg.BSCNodeURL)
router := newRouter()

return &Service{
cfg: *cfg,
router: router,
}
}

func (s *Service) Run() {
_ = http.ListenAndServe(s.cfg.HostPort, s.router)
}

func newRouter() *mux.Router {
r := mux.NewRouter()
r.Use(server.NormalizeQueryValuesHandler)
for _, e := range endpoints() {
r.HandleFunc(e.path, e.handler).Methods(e.methods...)
}
return r
}

type endpoint struct {
path string
handler http.HandlerFunc
methods []string
}

func endpoints() []endpoint {
return []endpoint{
{
path: versionMethod,
handler: VersionMethod,
methods: []string{http.MethodGet},
},
{
path: specMethod,
handler: SpecMethod,
methods: []string{http.MethodGet},
},
{
path: genesisMethod,
handler: GenesisMethod,
methods: []string{http.MethodGet},
},
{
path: sidecarsMethodPrefix,
handler: SidecarsMethod,
methods: []string{http.MethodGet},
},
}
}
95 changes: 95 additions & 0 deletions beacon/server_for_op_stack/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package server_for_op_stack

import (
"context"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/assert"
"strconv"
"testing"
)

func init() {
Init("https://bsc-mainnet.nodereal.io/v1/{api-key}")
}

func TestFetchBlockNumberByTime(t *testing.T) {
blockNum, err := fetchBlockNumberByTime(context.Background(), 1724052941, client)
assert.Nil(t, err)
assert.Equal(t, uint64(41493946), blockNum)

blockNum, err = fetchBlockNumberByTime(context.Background(), 1734052941, client)

Check failure on line 22 in beacon/server_for_op_stack/server_test.go

View workflow job for this annotation

GitHub Actions / golang-lint (1.21.x, ubuntu-latest)

ineffectual assignment to blockNum (ineffassign)
assert.Equal(t, err, errors.New("time too large"))

blockNum, err = fetchBlockNumberByTime(context.Background(), 1600153618, client)
assert.Nil(t, err)
assert.Equal(t, uint64(493946), blockNum)
}

func TestBeaconBlobSidecars(t *testing.T) {
indexBlobHash := []IndexedBlobHash{
{Hash: common.HexToHash("0x01231952ecbaede62f8d0398b656072c072db36982c9ef106fbbc39ce14f983c"), Index: 0},
{Hash: common.HexToHash("0x012c21a8284d2d707bb5318e874d2e1b97a53d028e96abb702b284a2cbb0f79c"), Index: 1},
{Hash: common.HexToHash("0x011196c8d02536ede0382aa6e9fdba6c460169c0711b5f97fcd701bd8997aee3"), Index: 2},
{Hash: common.HexToHash("0x019c86b46b27401fb978fd175d1eb7dadf4976d6919501b0c5280d13a5bab57b"), Index: 3},
{Hash: common.HexToHash("0x01e00db7ee99176b3fd50aab45b4fae953292334bbf013707aac58c455d98596"), Index: 4},
{Hash: common.HexToHash("0x0117d23b68123d578a98b3e1aa029661e0abda821a98444c21992eb1e5b7208f"), Index: 5},
//{Hash: common.HexToHash("0x01e00db7ee99176b3fd50aab45b4fae953292334bbf013707aac58c455d98596"), Index: 1},
}

resp, err := beaconBlobSidecars(context.Background(), 1724055046, []int{0, 1, 2, 3, 4, 5}) // block: 41494647
assert.Nil(t, err)
assert.NotNil(t, resp)
assert.NotEmpty(t, resp.Data)
for i, sideCar := range resp.Data {
assert.Equal(t, indexBlobHash[i].Index, sideCar.Index)
assert.Equal(t, indexBlobHash[i].Hash, kZGToVersionedHash(sideCar.KZGCommitment))
}

apiscs := make([]*BlobSidecar, 0, len(indexBlobHash))
// filter and order by hashes
for _, h := range indexBlobHash {
for _, apisc := range resp.Data {
if h.Index == int(apisc.Index) {

Check failure on line 54 in beacon/server_for_op_stack/server_test.go

View workflow job for this annotation

GitHub Actions / golang-lint (1.21.x, ubuntu-latest)

unnecessary conversion (unconvert)
apiscs = append(apiscs, apisc)
break
}
}
}

assert.Equal(t, len(apiscs), len(resp.Data))
assert.Equal(t, len(apiscs), len(indexBlobHash))
}

type TimeToSlotFn func(timestamp uint64) (uint64, error)

// GetTimeToSlotFn returns a function that converts a timestamp to a slot number.
func GetTimeToSlotFn(ctx context.Context) (TimeToSlotFn, error) {

Check failure on line 68 in beacon/server_for_op_stack/server_test.go

View workflow job for this annotation

GitHub Actions / golang-lint (1.21.x, ubuntu-latest)

unnecessary leading newline (whitespace)

genesis := beaconGenesis()
config := configSpec()

genesisTime, _ := strconv.ParseUint(genesis.Data.GenesisTime, 10, 64)
secondsPerSlot, _ := strconv.ParseUint(config.SecondsPerSlot, 10, 64)
if secondsPerSlot == 0 {
return nil, fmt.Errorf("got bad value for seconds per slot: %v", config.SecondsPerSlot)
}
timeToSlotFn := func(timestamp uint64) (uint64, error) {
if timestamp < genesisTime {
return 0, fmt.Errorf("provided timestamp (%v) precedes genesis time (%v)", timestamp, genesisTime)
}
return (timestamp - genesisTime) / secondsPerSlot, nil
}
return timeToSlotFn, nil
}

func TestAPI(t *testing.T) {
slotFn, err := GetTimeToSlotFn(context.Background())
assert.Nil(t, err)

expTx := uint64(123151345)
gotTx, err := slotFn(expTx)
assert.Nil(t, err)
assert.Equal(t, expTx, gotTx)
}
Loading

0 comments on commit bc5d39d

Please sign in to comment.