Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat) Add PreCodec modifier #961

Merged
merged 1 commit into from
Dec 13, 2024
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
67 changes: 67 additions & 0 deletions pkg/codec/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
// - epoch to time -> [EpochToTimeModifierConfig]
// - address to string -> [AddressBytesToStringModifierConfig]
// - field wrapper -> [WrapperModifierConfig]
// - precodec -> [PrecodecModifierConfig]
type ModifiersConfig []ModifierConfig

func (m *ModifiersConfig) UnmarshalJSON(data []byte) error {
Expand Down Expand Up @@ -58,6 +59,8 @@ func (m *ModifiersConfig) UnmarshalJSON(data []byte) error {
(*m)[i] = &AddressBytesToStringModifierConfig{}
case ModifierWrapper:
(*m)[i] = &WrapperModifierConfig{}
case ModifierPreCodec:
(*m)[i] = &PreCodecModifierConfig{}
default:
return fmt.Errorf("%w: unknown modifier type: %s", types.ErrInvalidConfig, mType)
}
Expand All @@ -84,6 +87,7 @@ func (m *ModifiersConfig) ToModifier(onChainHooks ...mapstructure.DecodeHookFunc
type ModifierType string

const (
ModifierPreCodec ModifierType = "precodec"
ModifierRename ModifierType = "rename"
ModifierDrop ModifierType = "drop"
ModifierHardCode ModifierType = "hard code"
Expand Down Expand Up @@ -199,6 +203,69 @@ func (h *HardCodeModifierConfig) MarshalJSON() ([]byte, error) {
})
}

// PreCodec creates a modifier that will transform data using a preliminary encoding/decoding step.
// 'Off-chain' values will be overwritten with the encoded data as a byte array.
// 'On-chain' values will be typed using the optimistic types from the codec.
// This is useful when wanting to move the data as generic bytes.
//
// Example:
//
// Based on this input struct:
// type example struct {
// A []B
// }
//
// type B struct {
// C string
// D string
// }
//
// And the fields config defined as:
// {"A": "string C, string D"}
//
// The codec config gives a map of strings (the values from fields config map) to implementation for encoding/decoding
//
// RemoteCodec {
// func (types.TypeProvider) CreateType(itemType string, forEncoding bool) (any, error)
// func (types.Decoder) Decode(ctx context.Context, raw []byte, into any, itemType string) error
// func (types.Encoder) Encode(ctx context.Context, item any, itemType string) ([]byte, error)
// func (types.Decoder) GetMaxDecodingSize(ctx context.Context, n int, itemType string) (int, error)
// func (types.Encoder) GetMaxEncodingSize(ctx context.Context, n int, itemType string) (int, error)
// }
//
// {"string C, string D": RemoteCodec}
//
// Result:
// type example struct {
// A [][]bytes
// }
//
// Where []bytes are the encoded input struct B
type PreCodecModifierConfig struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add more detailed documentation and probably examples in the doc?. Currently codec and codec modifiers are complex from an usability standpoint and it would be great to have more docs/examples for this new codec.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added an example! Let me know what else you think is missing and I can add more words

// A map of a path of properties to encoding scheme.
// If the path leads to an array, encoding will occur on every entry.
//
// Example: "a.b" -> "uint256 Value"
Fields map[string]string
// Codecs is skipped in JSON serialization, it will be injected later.
// The map should be keyed using the value from "Fields" to a corresponding Codec that can encode/decode for it
// This allows encoding and decoding implementations to be handled outside of the modifier.
//
// Example: "uint256 Value" -> a chain specific encoder for "uint256 Value"
Codecs map[string]types.RemoteCodec `json:"-"`
}

func (c *PreCodecModifierConfig) ToModifier(_ ...mapstructure.DecodeHookFunc) (Modifier, error) {
return NewPreCodec(c.Fields, c.Codecs)
}

func (c *PreCodecModifierConfig) MarshalJSON() ([]byte, error) {
return json.Marshal(&modifierMarshaller[PreCodecModifierConfig]{
Type: ModifierPreCodec,
T: c,
})
}

// EpochToTimeModifierConfig is used to convert epoch seconds as uint64 fields on-chain to time.Time
type EpochToTimeModifierConfig struct {
Fields []string
Expand Down
113 changes: 113 additions & 0 deletions pkg/codec/precodec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package codec

import (
"context"
"fmt"
"reflect"

"github.com/go-viper/mapstructure/v2"
"github.com/smartcontractkit/chainlink-common/pkg/types"
)

// PreCodec creates a modifier that will run a preliminary encoding/decoding step.
// This is useful when wanting to move nested data as generic bytes.
func NewPreCodec(fields map[string]string, codecs map[string]types.RemoteCodec) (Modifier, error) {
m := &preCodec{
modifierBase: modifierBase[string]{
fields: fields,
onToOffChainType: map[reflect.Type]reflect.Type{},
offToOnChainType: map[reflect.Type]reflect.Type{},
},
codecs: codecs,
}

// validate that there is a codec for each unique type definition
for _, typeDef := range fields {
if _, ok := m.codecs[typeDef]; ok {
continue
}
return nil, fmt.Errorf("codec not supplied for: %s", typeDef)
}

m.modifyFieldForInput = func(_ string, field *reflect.StructField, _ string, typeDef string) error {
if field.Type != reflect.SliceOf(reflect.TypeFor[uint8]()) {
return fmt.Errorf("can only decode []byte from on-chain: %s", field.Type)
}

codec, ok := m.codecs[typeDef]
if !ok || codec == nil {
return fmt.Errorf("codec not found for type definition: '%s'", typeDef)
}

newType, err := codec.CreateType("", false)
if err != nil {
return err
}
field.Type = reflect.TypeOf(newType)

return nil
}

return m, nil
}

type preCodec struct {
modifierBase[string]
codecs map[string]types.RemoteCodec
}

func (pc *preCodec) TransformToOffChain(onChainValue any, _ string) (any, error) {
allHooks := make([]mapstructure.DecodeHookFunc, 1)
allHooks[0] = hardCodeManyHook

return transformWithMaps(onChainValue, pc.onToOffChainType, pc.fields, pc.decodeFieldMapAction, allHooks...)
}

func (pc *preCodec) decodeFieldMapAction(extractMap map[string]any, key string, typeDef string) error {
_, exists := extractMap[key]
if !exists {
return fmt.Errorf("field %s does not exist", key)
}

codec, ok := pc.codecs[typeDef]
if !ok || codec == nil {
return fmt.Errorf("codec not found for type definition: '%s'", typeDef)
}

to, err := codec.CreateType("", false)
if err != nil {
return err
}
err = codec.Decode(context.Background(), extractMap[key].([]byte), &to, "")
if err != nil {
return err
}
extractMap[key] = to
return nil
}

func (pc *preCodec) TransformToOnChain(offChainValue any, _ string) (any, error) {
allHooks := make([]mapstructure.DecodeHookFunc, 1)
allHooks[0] = hardCodeManyHook

return transformWithMaps(offChainValue, pc.offToOnChainType, pc.fields, pc.encodeFieldMapAction, allHooks...)
}

func (pc *preCodec) encodeFieldMapAction(extractMap map[string]any, key string, typeDef string) error {
_, exists := extractMap[key]
if !exists {
return fmt.Errorf("field %s does not exist", key)
}

codec, ok := pc.codecs[typeDef]
if !ok || codec == nil {
return fmt.Errorf("codec not found for type definition: '%s'", typeDef)
}

encoded, err := codec.Encode(context.Background(), extractMap[key], "")
if err != nil {
return err
}
extractMap[key] = encoded
return nil
}
Loading
Loading