Skip to content

Commit

Permalink
beaconserver: a simulated beacon api server for op-stack. only necess…
Browse files Browse the repository at this point in the history
…ery apis realized.
  • Loading branch information
jhgdike committed Aug 22, 2024
1 parent c46d7e8 commit c9a78b3
Show file tree
Hide file tree
Showing 7 changed files with 473 additions and 0 deletions.
101 changes: 101 additions & 0 deletions beacon/beaconserver/api_func.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package beaconserver

import (
"context"
"sort"

"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"
)

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
}
88 changes: 88 additions & 0 deletions beacon/beaconserver/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package beaconserver

import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/prysmaticlabs/prysm/v5/api/server/structs"
"github.com/prysmaticlabs/prysm/v5/config/fieldparams"

Check failure on line 11 in beacon/beaconserver/handlers.go

View workflow job for this annotation

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

File is not `goimports`-ed (goimports)

Check failure on line 11 in beacon/beaconserver/handlers.go

View workflow job for this annotation

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

File is not `goimports`-ed (goimports)
"github.com/prysmaticlabs/prysm/v5/network/httputil"
)

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
}
93 changes: 93 additions & 0 deletions beacon/beaconserver/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package beaconserver

import (
"net/http"

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

const (
DefaultBSCNodeURL = "http://127.0.0.1:8545"
DefaultHostPort = "0.0.0.0:8686"
)

type Config struct {
Enable bool
BSCNodeURL string
HostPort string
}

func defaultConfig() *Config {
return &Config{
Enable: false,
BSCNodeURL: DefaultBSCNodeURL,
HostPort: DefaultHostPort,
}
}

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

func NewService(cfg *Config) *Service {
cfgs := defaultConfig()
if cfg.BSCNodeURL != "" {
cfgs.BSCNodeURL = cfg.BSCNodeURL
}
if cfg.HostPort != "" {
cfgs.HostPort = cfg.HostPort
}
Init(cfg.BSCNodeURL)
router := newRouter()

return &Service{
cfg: cfgs,
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},
},
}
}
96 changes: 96 additions & 0 deletions beacon/beaconserver/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package beaconserver

import (
"context"
"errors"
"fmt"
"strconv"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/assert"
)

func init() {
Init("https://bsc-dataseed1.binance.org/")
}

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 23 in beacon/beaconserver/server_test.go

View workflow job for this annotation

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

ineffectual assignment to blockNum (ineffassign)

Check failure on line 23 in beacon/beaconserver/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 55 in beacon/beaconserver/server_test.go

View workflow job for this annotation

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

unnecessary conversion (unconvert)

Check failure on line 55 in beacon/beaconserver/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 69 in beacon/beaconserver/server_test.go

View workflow job for this annotation

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

unnecessary leading newline (whitespace)

Check failure on line 69 in beacon/beaconserver/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 c9a78b3

Please sign in to comment.