Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions core/accessors_benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package core_test

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"

"github.com/NethermindEth/juno/adapters/sn2core"
"github.com/NethermindEth/juno/core"
"github.com/NethermindEth/juno/db"
"github.com/NethermindEth/juno/db/pebblev2"
_ "github.com/NethermindEth/juno/encoder/registry"
"github.com/NethermindEth/juno/starknet"
"github.com/stretchr/testify/require"
)

// Sampled ~1000 random mainnet blocks (heights 5M-11.5M) on 2026-07-07.
// These 3 are the closest match to that sample's p50/p95/p99 storage-diff count.
var stateUpdateWithBlockFixtures = []struct {
percentile string
file string
}{
{"p50", "5782224.json"},
{"p95", "8582459.json"},
{"p99", "9706496.json"},
}

type quietPebbleLogger struct{}

func (quietPebbleLogger) Infof(string, ...any) {}
func (quietPebbleLogger) Errorf(string, ...any) {}
func (quietPebbleLogger) Fatalf(format string, args ...any) {
panic(fmt.Sprintf(format, args...))
}

func newBenchDB(b *testing.B) db.KeyValueStore {
b.Helper()
database, err := pebblev2.New(b.TempDir(), pebblev2.WithLogger(quietPebbleLogger{}))
require.NoError(b, err)
b.Cleanup(func() { require.NoError(b, database.Close()) })
return database
}

func loadStateUpdateWithBlockFixture(b *testing.B, file string) (*core.Block, *core.StateUpdate) {
b.Helper()
path := filepath.Join("testdata", "accessors", "state_update_with_block", file)
data, err := os.ReadFile(path)
require.NoError(b, err)

var raw starknet.StateUpdateWithBlockAndSignature
require.NoError(b, json.Unmarshal(data, &raw))

block, err := sn2core.AdaptBlock(raw.Block, raw.Signature)
require.NoError(b, err)
su, err := sn2core.AdaptStateUpdate(raw.StateUpdate)
require.NoError(b, err)
return block, su
}

func BenchmarkReadStateUpdateByBlockNum_Mainnet(b *testing.B) {
for _, f := range stateUpdateWithBlockFixtures {
b.Run(f.percentile, func(b *testing.B) {
_, su := loadStateUpdateWithBlockFixture(b, f.file)
const blockNum = uint64(0)

database := newBenchDB(b)
require.NoError(b, core.WriteStateUpdateByBlockNum(database, blockNum, su))

_, err := core.GetStateUpdateByBlockNum(database, blockNum)
require.NoError(b, err)

b.ReportAllocs()
for b.Loop() {
if _, err := core.GetStateUpdateByBlockNum(database, blockNum); err != nil {
b.Fatal(err)
}
}
})
}
}

func BenchmarkReadBlockByNumber_Mainnet(b *testing.B) {
for _, f := range stateUpdateWithBlockFixtures {
b.Run(f.percentile, func(b *testing.B) {
block, _ := loadStateUpdateWithBlockFixture(b, f.file)
const blockNum = uint64(0)
block.Header.Number = blockNum

database := newBenchDB(b)
require.NoError(b, core.WriteBlockHeaderByNumber(database, block.Header))
require.NoError(b, core.WriteTransactionsAndReceipts(
database, blockNum, block.Transactions, block.Receipts,
))

_, err := core.GetBlockByNumber(database, blockNum)
require.NoError(b, err)

b.ReportAllocs()
for b.Loop() {
if _, err := core.GetBlockByNumber(database, blockNum); err != nil {
b.Fatal(err)
}
}
})
}
}
149 changes: 149 additions & 0 deletions core/felt/cbor.go
Comment thread
RafaelGranza marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package felt

import (
"encoding/binary"
"math"

"github.com/consensys/gnark-crypto/ecc/stark-curve/fp"
"github.com/fxamacker/cbor/v2"
)

// Fast, felt-specialized CBOR marshaling.
func (z *Felt) MarshalCBOR() ([]byte, error) {
return encodeFeltLimbs(z), nil
}

// Fast, felt-specialized CBOR unmarshaling.
// Falls back to the generic decoder on shape mismatch
func (z *Felt) UnmarshalCBOR(data []byte) error {
if decodeFeltLimbs(z, data) {
return nil
}
return cbor.Unmarshal(data, (*fp.Element)(z))
}

const (
// These derive from the CBOR spec
// Limb types are always unsigned int
// The following numbers represent the unsigned integer size
// See: https://www.rfc-editor.org/rfc/rfc8949.html#section-3
cborUint8AdditionalInfo = 24 // 1 byte follows
cborUint16AdditionalInfo = 25 // 2 bytes follow
cborUint32AdditionalInfo = 26 // 4 bytes follow
cborUint64AdditionalInfo = 27 // 8 bytes follow

// cborArrayHeader4 is the first byte of a CBOR array of Limbs items.
// Top 3 bits are the major type (4 = array), low 5 bits are the length.
// Type 4 in the top bits is the same as 1<<7, so this byte is
// 0b100_00100: an array with Limbs (4) elements.
cborArrayHeader4 = (1 << 7) | Limbs

// header + 8 bytes following
maxCBORUintLen = 1 + 8
)

func encodeFeltLimbs(value *Felt) []byte {
// The buffer format is: [cborArrayHeader4] [limb 0] [limb 1] [limb 2] [limb 3]
buffer := make([]byte, 1+Limbs*maxCBORUintLen)
buffer[0] = cborArrayHeader4

index := 1
for limbIndex := range Limbs {
limb := value[limbIndex]
switch {
case limb > math.MaxUint32:
buffer[index] = cborUint64AdditionalInfo
binary.BigEndian.PutUint64(buffer[index+1:], limb)
index += 1 + 8

case limb > math.MaxUint16:
buffer[index] = cborUint32AdditionalInfo
binary.BigEndian.PutUint32(buffer[index+1:], uint32(limb))
index += 1 + 4

case limb > math.MaxUint8:
buffer[index] = cborUint16AdditionalInfo
binary.BigEndian.PutUint16(buffer[index+1:], uint16(limb))
index += 1 + 2

case limb >= cborUint8AdditionalInfo:
buffer[index] = cborUint8AdditionalInfo
buffer[index+1] = byte(limb)
index += 2

default:
buffer[index] = byte(limb)
index++
}
}

return buffer[:index]
}

// Writes value only on success, so a rejected input can't partially
// corrupt it before falling back to the generic decoder.
func decodeFeltLimbs(value *Felt, data []byte) bool {
// The data format is: [cborArrayHeader4] [limb 0] [limb 1] [limb 2] [limb 3]
if len(data) == 0 || data[0] != cborArrayHeader4 {
return false
}

var limbs Felt
byteOffset := 1

for limbIndex := range Limbs {
if byteOffset >= len(data) {
return false
}

headerByte := data[byteOffset]
byteOffset++

var limb uint64

switch {
case headerByte > cborUint64AdditionalInfo: // invalid header byte for uint64
return false

case headerByte == cborUint64AdditionalInfo:
if byteOffset+8 > len(data) {
return false
}
limb = binary.BigEndian.Uint64(data[byteOffset:])
byteOffset += 8

case headerByte == cborUint32AdditionalInfo:
if byteOffset+4 > len(data) {
return false
}
limb = uint64(binary.BigEndian.Uint32(data[byteOffset:]))
byteOffset += 4

case headerByte == cborUint16AdditionalInfo:
if byteOffset+2 > len(data) {
return false
}
limb = uint64(binary.BigEndian.Uint16(data[byteOffset:]))
byteOffset += 2

case headerByte == cborUint8AdditionalInfo:
if byteOffset+1 > len(data) {
return false
}
limb = uint64(data[byteOffset])
byteOffset++

default: // headerByte < cborUint8AdditionalInfo
limb = uint64(headerByte)
}

limbs[limbIndex] = limb
}

if byteOffset != len(data) {
return false
}

*value = limbs
return true
}
Loading
Loading