Skip to content
Open
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
2 changes: 2 additions & 0 deletions node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
"github.com/NethermindEth/juno/starknet/compiler"
"github.com/NethermindEth/juno/sync"
"github.com/NethermindEth/juno/sync/preconfirmed"
"github.com/NethermindEth/juno/utils/cbor"
"github.com/NethermindEth/juno/utils/log"
"github.com/NethermindEth/juno/vm"
"github.com/consensys/gnark-crypto/ecc/stark-curve/ecdsa"
Expand Down Expand Up @@ -194,6 +195,7 @@ func New(cfg *Config, version string, logLevel *log.Level) (*Node, error) {
if err != nil {
return nil, err
}
cbor.SetLogger(logger)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With metrics this is avoidable, but this logger will be removed when fallback is removed.


if cfg.DisableSync {
if cfg.Sequencer {
Expand Down
21 changes: 17 additions & 4 deletions utils/cbor/cbor.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Package cbor provides Juno's CBOR API. Encoding and decoding currently use
// the fxamacker adapter.
// Package cbor provides Juno's CBOR API. Encoding uses fxamacker. Decoding uses the decoder
// registered for the destination's type with [RegisterDecoder].
package cbor

import (
Expand Down Expand Up @@ -31,12 +31,12 @@

// Unmarshal decodes a CBOR value from data into v.
func Unmarshal(data []byte, v any) error {
return fxamacker.Unmarshal(data, v)
return decoderFor(v).unmarshal(data, v)
}

// UnmarshalFirst decodes the first CBOR item and returns the remaining bytes.
func UnmarshalFirst(data []byte, v any) ([]byte, error) {
return fxamacker.UnmarshalFirst(data, v)
return decoderFor(v).unmarshalFirst(data, v)
}

// UnmarshalStrict decodes without type tags and rejects unknown fields.
Expand All @@ -55,3 +55,16 @@
func RegisterType(t reflect.Type) error {
return fxamacker.RegisterType(t)
}

// RegisterDecoder makes decode the decoder of *T and **T destinations.
// decode reads one item from the start of data into a dst-owned item and returns the rest.
// If it fails, the item is decoded with fxamacker instead.
// Call it only from init, and decode must be safe for concurrent use.
func RegisterDecoder[T any](decode func(data []byte, dst *T) (rest []byte, err error)) {
Comment thread
danielntmd marked this conversation as resolved.
Resolved
value, pointer := typeWord(new(T)), typeWord(new(*T))
if decoders[value] != nil || decoders[pointer] != nil {
panic("cbor: a decoder is already registered for " + reflect.TypeFor[T]().String())

Check warning on line 66 in utils/cbor/cbor.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/cbor.go#L64-L66

Added lines #L64 - L66 were not covered by tests
}
decoders[value] = valueDecoder(decode)
decoders[pointer] = pointerDecoder(decode)

Check warning on line 69 in utils/cbor/cbor.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/cbor.go#L68-L69

Added lines #L68 - L69 were not covered by tests
}
Comment thread
danielntmd marked this conversation as resolved.
Resolved
173 changes: 173 additions & 0 deletions utils/cbor/decoders.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package cbor

import (
"bytes"
"fmt"
"reflect"
"sync/atomic"
"time"
"unsafe"

"github.com/NethermindEth/juno/utils/cbor/fxamacker"
"github.com/NethermindEth/juno/utils/log"
"go.uber.org/zap"
)

// A decoder reads CBOR into a destination.
type decoder struct {
unmarshal func(data []byte, v any) error
unmarshalFirst func(data []byte, v any) ([]byte, error)
}

// fxamackerDecoder is the default decoder.
// It decodes types with no registered decoder and is the current fallback.
var fxamackerDecoder = decoder{fxamacker.Unmarshal, fxamacker.UnmarshalFirst}

// decoders finds the decoder of a destination by its type, without hashing the interface. A
// destination is looked up by its exact pointer type, *T or **T, so the pointer inside the
// interface is always the one its decoder was written for, and the unsafe casts only erase and
// restore that type. This assumes gc's layout of an interface (the type word followed by the data
// word), which [reflect] also relies on.
var decoders = map[unsafe.Pointer]*typeDecoder{}

// decoderFor returns the decoder of v.
// Either the one registered for its type, or fxamacker.
func decoderFor(v any) decoder {
value := (*eface)(unsafe.Pointer(&v))
d, ok := decoders[value.typ]
if !ok || value.data == nil || !d.isZero(value.data) {
return fxamackerDecoder
}
return d.decoder

Check warning on line 41 in utils/cbor/decoders.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/decoders.go#L41

Added line #L41 was not covered by tests
}
Comment thread
danielntmd marked this conversation as resolved.
Resolved
Comment thread
danielntmd marked this conversation as resolved.
Resolved

// eface is the layout of an interface value with no methods.
type eface struct {
typ, data unsafe.Pointer
}

// typeWord returns the dynamic type of v as the runtime keeps it.
func typeWord(v any) unsafe.Pointer {
return (*eface)(unsafe.Pointer(&v)).typ

Check warning on line 51 in utils/cbor/decoders.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/decoders.go#L51

Added line #L51 was not covered by tests
}

// dest returns the pointer that v, a pointer to a registered type, holds.
func dest(v any) unsafe.Pointer {
return (*eface)(unsafe.Pointer(&v)).data

Check warning on line 56 in utils/cbor/decoders.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/decoders.go#L56

Added line #L56 was not covered by tests
}

// A typeDecoder decodes into destinations of one pointer type, *T or **T.
// Registering a decoder for T creates one of each.
type typeDecoder struct {
name string // destination type
// decoder is what decoderFor returns for the type, built by bind.
decoder decoder
// decodeUnsafe is the registered decoder with the destination as the pointer itself.
decodeUnsafe func(data []byte, dst unsafe.Pointer) ([]byte, error)
// reset zeroes dst, undoing a partial read.
reset func(dst unsafe.Pointer)
// isZero reports whether dst holds the zero value, which for **T is a nil pointer.
isZero func(dst unsafe.Pointer) bool
}

// logger reports failed decodes.
var logger atomic.Pointer[log.StructuredLogger]

// SetLogger sets where a decode that falls back to fxamacker is reported. The reports are sampled,
// so a hot read path cannot flood the log. Call it once, at startup.
// todo(danielntmd): It reports errors only, not a wrong value returned without one. The fallback
// and this logger are meant to be removed eventually.
func SetLogger(l log.StructuredLogger) {
// The first few reports in each interval, then every thereafter'th.
const (
interval = time.Minute
first = 5
thereafter = 100
)
l = log.Sampled(l, interval, first, thereafter)
logger.Store(&l)
}

// unmarshalFirst reads one item into v and returns the bytes after it.
func (d *typeDecoder) unmarshalFirst(data []byte, v any) ([]byte, error) {
rest, err := d.decodeUnsafe(data, dest(v))
if err != nil {
if l := logger.Load(); l != nil {
(*l).Warn("cbor: decoder failed, decoding with fxamacker",
zap.String("type", d.name), zap.Error(err))

Check warning on line 97 in utils/cbor/decoders.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/decoders.go#L93-L97

Added lines #L93 - L97 were not covered by tests
}
d.reset(dest(v))
return fxamackerDecoder.unmarshalFirst(data, v) // fallback

Check warning on line 100 in utils/cbor/decoders.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/decoders.go#L99-L100

Added lines #L99 - L100 were not covered by tests
}
return rest, nil

Check warning on line 102 in utils/cbor/decoders.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/decoders.go#L102

Added line #L102 was not covered by tests
}

// unmarshal reads exactly one item into v.
func (d *typeDecoder) unmarshal(data []byte, v any) error {
rest, err := d.unmarshalFirst(data, v)
if err == nil && len(rest) != 0 {
d.reset(dest(v))
return fmt.Errorf("cbor: %d bytes of extraneous data", len(rest))

Check warning on line 110 in utils/cbor/decoders.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/decoders.go#L107-L110

Added lines #L107 - L110 were not covered by tests
}
return err

Check warning on line 112 in utils/cbor/decoders.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/decoders.go#L112

Added line #L112 was not covered by tests
}

// bind builds the decoder that decoderFor returns for d, once, so that a decode allocates nothing
// to call it.
func bind(d *typeDecoder) *typeDecoder {
d.decoder = decoder{d.unmarshal, d.unmarshalFirst}
return d

Check warning on line 119 in utils/cbor/decoders.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/decoders.go#L118-L119

Added lines #L118 - L119 were not covered by tests
}

// valueDecoder returns the decoder of *T destinations.
func valueDecoder[T any](decode func([]byte, *T) ([]byte, error)) *typeDecoder {
typ := reflect.TypeFor[T]()
zeroBytes := make([]byte, typ.Size())
return bind(&typeDecoder{
name: reflect.TypeFor[*T]().String(),

Check warning on line 127 in utils/cbor/decoders.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/decoders.go#L124-L127

Added lines #L124 - L127 were not covered by tests
// *T and unsafe.Pointer are passed the same way, so the decoder is called directly.
decodeUnsafe: *(*func([]byte, unsafe.Pointer) ([]byte, error))(unsafe.Pointer(&decode)),
Comment thread
danielntmd marked this conversation as resolved.
Resolved
reset: func(dst unsafe.Pointer) {
*(*T)(dst) = *new(T)

Check warning on line 131 in utils/cbor/decoders.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/decoders.go#L129-L131

Added lines #L129 - L131 were not covered by tests
},
// All-zero bytes are the zero value, but not the reverse (an empty string can still
// point somewhere), so [reflect] decides the rest.
isZero: func(dst unsafe.Pointer) bool {
return bytes.Equal(unsafe.Slice((*byte)(dst), len(zeroBytes)), zeroBytes) ||
reflect.NewAt(typ, dst).Elem().IsZero()

Check warning on line 137 in utils/cbor/decoders.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/decoders.go#L136-L137

Added lines #L136 - L137 were not covered by tests
},
})
}

// Head bytes of the two items a pointer treats as nil.
// (RFC 8949, section 3.3)
const (
simpleNull = 0xf6
simpleUndefined = 0xf7
)

// pointerDecoder returns the decoder of **T destinations.
func pointerDecoder[T any](decode func([]byte, *T) ([]byte, error)) *typeDecoder {
return bind(&typeDecoder{
name: reflect.TypeFor[**T]().String(),
decodeUnsafe: func(data []byte, dst unsafe.Pointer) ([]byte, error) {

Check warning on line 153 in utils/cbor/decoders.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/decoders.go#L151-L153

Added lines #L151 - L153 were not covered by tests
// For parity with fxamacker, null and undefined leave the pointer nil.
if len(data) > 0 && (data[0] == simpleNull || data[0] == simpleUndefined) {
return data[1:], nil

Check warning on line 156 in utils/cbor/decoders.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/decoders.go#L155-L156

Added lines #L155 - L156 were not covered by tests
}
value := new(T)
rest, err := decode(data, value)
if err != nil {
return nil, err

Check warning on line 161 in utils/cbor/decoders.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/decoders.go#L158-L161

Added lines #L158 - L161 were not covered by tests
}
*(**T)(dst) = value
return rest, nil

Check warning on line 164 in utils/cbor/decoders.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/decoders.go#L163-L164

Added lines #L163 - L164 were not covered by tests
},
reset: func(dst unsafe.Pointer) {
*(**T)(dst) = nil

Check warning on line 167 in utils/cbor/decoders.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/decoders.go#L167

Added line #L167 was not covered by tests
},
isZero: func(dst unsafe.Pointer) bool {
return *(**T)(dst) == nil

Check warning on line 170 in utils/cbor/decoders.go

View check run for this annotation

Codecov / codecov/patch

utils/cbor/decoders.go#L170

Added line #L170 was not covered by tests
},
})
}
Loading