diff --git a/abi/README.md b/abi/README.md index c3091e73ec..a790d7364c 100644 --- a/abi/README.md +++ b/abi/README.md @@ -1,56 +1,62 @@ # ABI Package ## Overview -The ABI package provides functionality for marshaling and unmarshaling actions. It is designed to work across different language implementations. +The ABI package provides functionality for marshaling and unmarshaling actions. +It uses the [Canoto](https://github.com/StephenButtolph/canoto) serialization +protocol and is designed to work across different language implementations. ## ABI Format The ABI is defined in JSON format, as shown in the `abi.json` file: ```json { - "actions": [ + "actionsSpec": [ { - "id": 1, - "action": "MockObjectSingleNumber" - }, + "name": "TestAction", + "fields": [ + { + "fieldNumber": 1, + "name": "NumComputeUnits", + "typeUint": 4 + }, + + ] + } ], - "types": [ + "outputsSpec": [ { - "name": "MockObjectSingleNumber", + "name": "TestOutput", "fields": [ { - "name": "Field1", - "type": "uint16" - } + "fieldNumber": 1, + "name": "Bytes", + "typeBytes": true + }, ] - }, + } + ], + "actionTypes": [ + { + "name": "TestAction", + "id": 0 + } + ], + "outputTypes": [ + { + "name": "TestOutput", + "id": 0 + } ] } -``` - -The ABI consists of two main sections: -- actions: A list of action definitions, each with a typeID and action name (action name specifies the type) -- types: A dictionary of types including their name and corresponding fields - -The type in each field must either be included in the ABI's `types` or in the list of [Supported Primitive Types](#supported-primitive-types). - -## Test Vectors -This implementation provides `testdata/` for implementations in any other language. -To verify correctness, an implementation can implement the following pseudocode: ``` -abi = abi.json -for filename in testdata/*.hex: - if filename.endswith(".hash.hex"): - continue - expectedHex = readFile(filename) - json = readFile(filename.replace(".hex", ".json")) +The ABI consists of the following sections: +- `actionsSpec`: a list of action definitions, according to the Canoto specification +- `outputsSpec`: a list of output definitions, according to the Canoto specification +- `actionTypes`: a dictionary of all available actions and their corresponding typeIDs +- `outputTypes`: a dictionary of all available outputs and their corresponding typeIDs - actualHex = Marshal(abi, json) - if actualHex != expectedHex: - raise "Hex values do not match" - -``` +The type in an action/output field must be a [supported canoto type](https://github.com/StephenButtolph/canoto?tab=readme-ov-file#supported-types). ## ABI Verification Frontends can use the ABI to display proper action and field names. For a wallet to verify it knows what it's signing, it must ensure that a canonical hash of the ABI is included in the message it signs. @@ -58,41 +64,3 @@ Frontends can use the ABI to display proper action and field names. For a wallet A correct VM will verify the signature against the same ABI hash, such that verification fails if the wallet signed an action against a different than expected ABI. This enables frontends to provide a verifiable display of what they are asking users to sign. - -## Constraints -- Actions require an ID, other structs / types do not require one -- Multiple structs with the same name from different packages are not supported -- Maps are not supported; use slices or arrays instead -- Built-in type `codec.Address` included as a special case - -## Generating Golang Bindings -Use cmd/abigen to automatically generate Go bindings from an ABI's JSON. - -For example, to auto-generate golang bindings for the test ABI provided in `./abi/testdata/abi.json` run: - -```sh -go run ./cmd/abigen/ ./abi/testdata/abi.json ./example.go --package=testpackage -``` - -This should generate the same code that is present in `./abi/mockabi_test.go`. - -## Supported Primitive Types - -| Type | Range/Description | JSON Serialization | Binary Serialization | -|-----------|----------------------------------------------------------|--------------------|---------------------------------------| -| `bool` | true or false | boolean | 1 byte | -| `uint8` | numbers from 0 to 255 | number | 1 byte | -| `uint16` | numbers from 0 to 65535 | number | 2 bytes | -| `uint32` | numbers from 0 to 4294967295 | number | 4 bytes | -| `uint64` | numbers from 0 to 18446744073709551615 | number | 8 bytes | -| `int8` | numbers from -128 to 127 | number | 1 byte | -| `int16` | numbers from -32768 to 32767 | number | 2 bytes | -| `int32` | numbers from -2147483648 to 2147483647 | number | 4 bytes | -| `int64` | numbers from -9223372036854775808 to 9223372036854775807 | number | 8 bytes | -| `Address` | 33 byte array | base64 | 33 bytes | -| `string` | string | string | uint16 length + bytes | -| `[]T` | for any `T` in the above list, serialized as an array | array | uint32 length + elements | -| `[x]T` | for any `T` in the above list, serialized as an array | array | uint32 length + elements | -| `[]uint8` | byte slice | base64 | uint32 length + bytes | -| `[x]uint8`| byte array | array of numbers | x bytes | - diff --git a/abi/abi.canoto.go b/abi/abi.canoto.go new file mode 100644 index 0000000000..8c87c6b236 --- /dev/null +++ b/abi/abi.canoto.go @@ -0,0 +1,473 @@ +// Code generated by canoto. DO NOT EDIT. +// versions: +// canoto v0.15.0 +// source: abi.go + +package abi + +import ( + "io" + "reflect" + "sync/atomic" + + "github.com/StephenButtolph/canoto" +) + +// Ensure that unused imports do not error +var ( + _ atomic.Uint64 + + _ = io.ErrUnexpectedEOF +) + +const ( + canoto__ABI__ActionsSpec__tag = "\x0a" // canoto.Tag(1, canoto.Len) + canoto__ABI__OutputsSpec__tag = "\x12" // canoto.Tag(2, canoto.Len) + canoto__ABI__ActionTypes__tag = "\x1a" // canoto.Tag(3, canoto.Len) + canoto__ABI__OutputTypes__tag = "\x22" // canoto.Tag(4, canoto.Len) +) + +type canotoData_ABI struct { + size atomic.Uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*ABI) CanotoSpec(types ...reflect.Type) *canoto.Spec { + types = append(types, reflect.TypeOf(ABI{})) + var zero ABI + s := &canoto.Spec{ + Name: "ABI", + Fields: []canoto.FieldType{ + canoto.FieldTypeFromField( + /*type inference:*/ (canoto.MakeEntry(zero.ActionsSpec)), + /*FieldNumber: */ 1, + /*Name: */ "ActionsSpec", + /*FixedLength: */ 0, + /*Repeated: */ true, + /*OneOf: */ "", + /*types: */ types, + ), + canoto.FieldTypeFromField( + /*type inference:*/ (canoto.MakeEntry(zero.OutputsSpec)), + /*FieldNumber: */ 2, + /*Name: */ "OutputsSpec", + /*FixedLength: */ 0, + /*Repeated: */ true, + /*OneOf: */ "", + /*types: */ types, + ), + canoto.FieldTypeFromField( + /*type inference:*/ (canoto.MakeEntryNilPointer(zero.ActionTypes)), + /*FieldNumber: */ 3, + /*Name: */ "ActionTypes", + /*FixedLength: */ 0, + /*Repeated: */ true, + /*OneOf: */ "", + /*types: */ types, + ), + canoto.FieldTypeFromField( + /*type inference:*/ (canoto.MakeEntryNilPointer(zero.OutputTypes)), + /*FieldNumber: */ 4, + /*Name: */ "OutputTypes", + /*FixedLength: */ 0, + /*Repeated: */ true, + /*OneOf: */ "", + /*types: */ types, + ), + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*ABI) MakeCanoto() *ABI { + return new(ABI) +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *ABI) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a canoto.Reader. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *ABI) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = ABI{} + c.canotoData.size.Store(uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case 1: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Read the first entry manually because the tag is already + // stripped. + originalUnsafe := r.Unsafe + r.Unsafe = true + var msgBytes []byte + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + r.Unsafe = originalUnsafe + + // Count the number of additional entries after the first entry. + countMinus1, err := canoto.CountBytes(r.B, canoto__ABI__ActionsSpec__tag) + if err != nil { + return err + } + + c.ActionsSpec = canoto.MakeSlice(c.ActionsSpec, countMinus1+1) + if len(msgBytes) != 0 { + remainingBytes := r.B + r.B = msgBytes + c.ActionsSpec[0] = canoto.MakePointer(c.ActionsSpec[0]) + if err := (c.ActionsSpec[0]).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + } + + // Read the rest of the entries, stripping the tag each time. + for i := range countMinus1 { + r.B = r.B[len(canoto__ABI__ActionsSpec__tag):] + r.Unsafe = true + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + if len(msgBytes) == 0 { + continue + } + r.Unsafe = originalUnsafe + + remainingBytes := r.B + r.B = msgBytes + c.ActionsSpec[1+i] = canoto.MakePointer(c.ActionsSpec[1+i]) + if err := (c.ActionsSpec[1+i]).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + } + case 2: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Read the first entry manually because the tag is already + // stripped. + originalUnsafe := r.Unsafe + r.Unsafe = true + var msgBytes []byte + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + r.Unsafe = originalUnsafe + + // Count the number of additional entries after the first entry. + countMinus1, err := canoto.CountBytes(r.B, canoto__ABI__OutputsSpec__tag) + if err != nil { + return err + } + + c.OutputsSpec = canoto.MakeSlice(c.OutputsSpec, countMinus1+1) + if len(msgBytes) != 0 { + remainingBytes := r.B + r.B = msgBytes + c.OutputsSpec[0] = canoto.MakePointer(c.OutputsSpec[0]) + if err := (c.OutputsSpec[0]).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + } + + // Read the rest of the entries, stripping the tag each time. + for i := range countMinus1 { + r.B = r.B[len(canoto__ABI__OutputsSpec__tag):] + r.Unsafe = true + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + if len(msgBytes) == 0 { + continue + } + r.Unsafe = originalUnsafe + + remainingBytes := r.B + r.B = msgBytes + c.OutputsSpec[1+i] = canoto.MakePointer(c.OutputsSpec[1+i]) + if err := (c.OutputsSpec[1+i]).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + } + case 3: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Read the first entry manually because the tag is already + // stripped. + originalUnsafe := r.Unsafe + r.Unsafe = true + var msgBytes []byte + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + r.Unsafe = originalUnsafe + + // Count the number of additional entries after the first entry. + countMinus1, err := canoto.CountBytes(r.B, canoto__ABI__ActionTypes__tag) + if err != nil { + return err + } + + c.ActionTypes = canoto.MakeSlice(c.ActionTypes, countMinus1+1) + if len(msgBytes) != 0 { + remainingBytes := r.B + r.B = msgBytes + if err := (&c.ActionTypes[0]).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + } + + // Read the rest of the entries, stripping the tag each time. + for i := range countMinus1 { + r.B = r.B[len(canoto__ABI__ActionTypes__tag):] + r.Unsafe = true + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + if len(msgBytes) == 0 { + continue + } + r.Unsafe = originalUnsafe + + remainingBytes := r.B + r.B = msgBytes + if err := (&c.ActionTypes[1+i]).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + } + case 4: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Read the first entry manually because the tag is already + // stripped. + originalUnsafe := r.Unsafe + r.Unsafe = true + var msgBytes []byte + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + r.Unsafe = originalUnsafe + + // Count the number of additional entries after the first entry. + countMinus1, err := canoto.CountBytes(r.B, canoto__ABI__OutputTypes__tag) + if err != nil { + return err + } + + c.OutputTypes = canoto.MakeSlice(c.OutputTypes, countMinus1+1) + if len(msgBytes) != 0 { + remainingBytes := r.B + r.B = msgBytes + if err := (&c.OutputTypes[0]).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + } + + // Read the rest of the entries, stripping the tag each time. + for i := range countMinus1 { + r.B = r.B[len(canoto__ABI__OutputTypes__tag):] + r.Unsafe = true + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + if len(msgBytes) == 0 { + continue + } + r.Unsafe = originalUnsafe + + remainingBytes := r.B + r.B = msgBytes + if err := (&c.OutputTypes[1+i]).UnmarshalCanotoFrom(r); err != nil { + return err + } + r.B = remainingBytes + } + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *ABI) ValidCanoto() bool { + if c == nil { + return true + } + for i := range c.ActionsSpec { + if c.ActionsSpec[i] != nil && !(c.ActionsSpec[i]).ValidCanoto() { + return false + } + } + for i := range c.OutputsSpec { + if c.OutputsSpec[i] != nil && !(c.OutputsSpec[i]).ValidCanoto() { + return false + } + } + for i := range c.ActionTypes { + if !(&c.ActionTypes[i]).ValidCanoto() { + return false + } + } + for i := range c.OutputTypes { + if !(&c.OutputTypes[i]).ValidCanoto() { + return false + } + } + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +func (c *ABI) CalculateCanotoCache() { + if c == nil { + return + } + var size uint64 + for i := range c.ActionsSpec { + var fieldSize uint64 + if c.ActionsSpec[i] != nil { + (c.ActionsSpec[i]).CalculateCanotoCache() + fieldSize = (c.ActionsSpec[i]).CachedCanotoSize() + } + size += uint64(len(canoto__ABI__ActionsSpec__tag)) + canoto.SizeUint(fieldSize) + fieldSize + } + for i := range c.OutputsSpec { + var fieldSize uint64 + if c.OutputsSpec[i] != nil { + (c.OutputsSpec[i]).CalculateCanotoCache() + fieldSize = (c.OutputsSpec[i]).CachedCanotoSize() + } + size += uint64(len(canoto__ABI__OutputsSpec__tag)) + canoto.SizeUint(fieldSize) + fieldSize + } + for i := range c.ActionTypes { + (&c.ActionTypes[i]).CalculateCanotoCache() + fieldSize := (&c.ActionTypes[i]).CachedCanotoSize() + size += uint64(len(canoto__ABI__ActionTypes__tag)) + canoto.SizeUint(fieldSize) + fieldSize + } + for i := range c.OutputTypes { + (&c.OutputTypes[i]).CalculateCanotoCache() + fieldSize := (&c.OutputTypes[i]).CachedCanotoSize() + size += uint64(len(canoto__ABI__OutputTypes__tag)) + canoto.SizeUint(fieldSize) + fieldSize + } + c.canotoData.size.Store(size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *ABI) CachedCanotoSize() uint64 { + if c == nil { + return 0 + } + return c.canotoData.size.Load() +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *ABI) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a canoto.Writer and returns the +// resulting canoto.Writer. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *ABI) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if c == nil { + return w + } + for i := range c.ActionsSpec { + canoto.Append(&w, canoto__ABI__ActionsSpec__tag) + var fieldSize uint64 + if c.ActionsSpec[i] != nil { + fieldSize = (c.ActionsSpec[i]).CachedCanotoSize() + } + canoto.AppendUint(&w, fieldSize) + if fieldSize != 0 { + w = (c.ActionsSpec[i]).MarshalCanotoInto(w) + } + } + for i := range c.OutputsSpec { + canoto.Append(&w, canoto__ABI__OutputsSpec__tag) + var fieldSize uint64 + if c.OutputsSpec[i] != nil { + fieldSize = (c.OutputsSpec[i]).CachedCanotoSize() + } + canoto.AppendUint(&w, fieldSize) + if fieldSize != 0 { + w = (c.OutputsSpec[i]).MarshalCanotoInto(w) + } + } + for i := range c.ActionTypes { + canoto.Append(&w, canoto__ABI__ActionTypes__tag) + canoto.AppendUint(&w, (&c.ActionTypes[i]).CachedCanotoSize()) + w = (&c.ActionTypes[i]).MarshalCanotoInto(w) + } + for i := range c.OutputTypes { + canoto.Append(&w, canoto__ABI__OutputTypes__tag) + canoto.AppendUint(&w, (&c.OutputTypes[i]).CachedCanotoSize()) + w = (&c.OutputTypes[i]).MarshalCanotoInto(w) + } + return w +} diff --git a/abi/abi.go b/abi/abi.go index 1004b9c812..e1e58bb824 100644 --- a/abi/abi.go +++ b/abi/abi.go @@ -3,245 +3,92 @@ package abi -import ( - "fmt" - "reflect" - "strings" +//go:generate go run github.com/StephenButtolph/canoto/canoto $GOFILE - "github.com/ava-labs/avalanchego/utils/set" +import ( + "github.com/StephenButtolph/canoto" + "github.com/ava-labs/hypersdk/chain" "github.com/ava-labs/hypersdk/codec" ) type ABI struct { - Actions []TypedStruct `serialize:"true" json:"actions"` - Outputs []TypedStruct `serialize:"true" json:"outputs"` - Types []Type `serialize:"true" json:"types"` -} + ActionsSpec []*canoto.Spec `canoto:"repeated pointer,1" json:"actionsSpec"` + OutputsSpec []*canoto.Spec `canoto:"repeated pointer,2" json:"outputsSpec"` -var _ codec.Typed = (*ABI)(nil) + ActionTypes []codec.TypedStruct `canoto:"repeated value,3" json:"actionTypes"` + OutputTypes []codec.TypedStruct `canoto:"repeated value,4" json:"outputTypes"` -func (ABI) GetTypeID() uint8 { - return 0 + canotoData canotoData_ABI } -type Field struct { - Name string `serialize:"true" json:"name"` - Type string `serialize:"true" json:"type"` -} - -type TypedStruct struct { - ID uint8 `serialize:"true" json:"id"` - Name string `serialize:"true" json:"name"` -} - -type Type struct { - Name string `serialize:"true" json:"name"` - Fields []Field `serialize:"true" json:"fields"` -} - -func NewABI(actions []codec.Typed, returnTypes []codec.Typed) (ABI, error) { - vmActions := make([]TypedStruct, 0) - vmOutputs := make([]TypedStruct, 0) - vmTypes := make([]Type, 0) - typesSet := set.Set[string]{} - typesAlreadyProcessed := set.Set[reflect.Type]{} - - for _, action := range actions { - actionABI, typeABI, err := describeTypedStruct(action, typesAlreadyProcessed) - if err != nil { - return ABI{}, err - } - vmActions = append(vmActions, actionABI) - for _, t := range typeABI { - if !typesSet.Contains(t.Name) { - vmTypes = append(vmTypes, t) - typesSet.Add(t.Name) - } - } - } - - for _, returnType := range returnTypes { - outputABI, typeABI, err := describeTypedStruct(returnType, typesAlreadyProcessed) - if err != nil { - return ABI{}, err - } - vmOutputs = append(vmOutputs, outputABI) - for _, t := range typeABI { - if !typesSet.Contains(t.Name) { - vmTypes = append(vmTypes, t) - typesSet.Add(t.Name) - } - } +func NewABI(actionParser *codec.CanotoParser[chain.Action], outputParser *codec.CanotoParser[codec.Typed]) *ABI { + return &ABI{ + ActionsSpec: actionParser.GetRegisteredTypes(), + OutputsSpec: outputParser.GetRegisteredTypes(), + ActionTypes: actionParser.GetTypedStructs(), + OutputTypes: outputParser.GetTypedStructs(), } - - return ABI{Actions: vmActions, Outputs: vmOutputs, Types: vmTypes}, nil } -// describeTypedStruct generates the TypedStruct and Types for a single typed struct (action or output). -// It handles both struct and pointer types, and recursively processes nested structs. -// Does not support maps or interfaces - only standard go types, slices, arrays and structs - -func describeTypedStruct(typedStruct codec.Typed, typesAlreadyProcessed set.Set[reflect.Type]) (TypedStruct, []Type, error) { - t := reflect.TypeOf(typedStruct) - if t.Kind() == reflect.Ptr { - t = t.Elem() +func (t *ABI) CalculateCanotoSpec() { + for i := range t.ActionsSpec { + t.ActionsSpec[i].CalculateCanotoCache() } - - typedStructABI := TypedStruct{ - ID: typedStruct.GetTypeID(), - Name: t.Name(), + for i := range t.OutputsSpec { + t.OutputsSpec[i].CalculateCanotoCache() } - - typesABI := make([]Type, 0) - typesLeft := []reflect.Type{t} - - // Process all types, including nested ones - for { - var nextType reflect.Type - nextTypeFound := false - for _, anotherType := range typesLeft { - if !typesAlreadyProcessed.Contains(anotherType) { - nextType = anotherType - nextTypeFound = true - break - } - } - if !nextTypeFound { - break - } - - fields, moreTypes, err := describeStruct(nextType) - if err != nil { - return TypedStruct{}, nil, err - } - - typesABI = append(typesABI, Type{ - Name: nextType.Name(), - Fields: fields, - }) - typesLeft = append(typesLeft, moreTypes...) - - typesAlreadyProcessed.Add(nextType) - } - - return typedStructABI, typesABI, nil } -// describeStruct analyzes a struct type and returns its fields and any nested struct types it found -func describeStruct(t reflect.Type) ([]Field, []reflect.Type, error) { - kind := t.Kind() - - if kind != reflect.Struct { - return nil, nil, fmt.Errorf("type %s is not a struct", t) - } - - fields := make([]Field, 0) - otherStructsSeen := make([]reflect.Type, 0) - - for i := 0; i < t.NumField(); i++ { - field := t.Field(i) - fieldType := field.Type - fieldName := field.Name - - // Skip any field that will not be serialized by the codec - serializeTag := field.Tag.Get("serialize") - if serializeTag != "true" { - continue - } - - // Handle JSON tag for field name override - jsonTag := field.Tag.Get("json") - if jsonTag != "" { - parts := strings.Split(jsonTag, ",") - fieldName = parts[0] - } - - if field.Anonymous && fieldType.Kind() == reflect.Struct { - // Handle embedded struct by flattening its fields - embeddedFields, moreTypes, err := describeStruct(fieldType) - if err != nil { - return nil, nil, err - } - fields = append(fields, embeddedFields...) - otherStructsSeen = append(otherStructsSeen, moreTypes...) - } else { - arrayPrefix := "" - - // Here we assume that all types without a name are slices or arrays. - // We completely ignore the fact that maps exist as we don't support them. - // Types like `type Address = [33]byte` are arrays technically, but they have a name - // and we need them to be named types instead of slices. - for fieldType.Name() == "" { - if fieldType.Kind() == reflect.Array { - arrayPrefix += fmt.Sprintf("[%d]", fieldType.Len()) - fieldType = fieldType.Elem() - } else { - arrayPrefix += "[]" - fieldType = fieldType.Elem() - } - } - - typeName := arrayPrefix + fieldType.Name() - - // Add nested structs and pointers to structs to the list for processing - if fieldType.Kind() == reflect.Struct { - otherStructsSeen = append(otherStructsSeen, fieldType) - } else if fieldType.Kind() == reflect.Ptr { - otherStructsSeen = append(otherStructsSeen, fieldType.Elem()) - } - - fields = append(fields, Field{ - Name: fieldName, - Type: typeName, - }) +func (t *ABI) FindActionSpecByName(name string) (*canoto.Spec, bool) { + for _, spec := range t.ActionsSpec { + if spec.Name == name { + return spec, true } } - - return fields, otherStructsSeen, nil + return nil, false } -func (a *ABI) FindOutputByID(id uint8) (TypedStruct, bool) { - for _, output := range a.Outputs { - if output.ID == id { - return output, true +func (t *ABI) FindOutputSpecByID(id uint8) (*canoto.Spec, bool) { + var ( + typeName string + found bool + ) + for i := range t.OutputTypes { + if t.OutputTypes[i].ID == id { + typeName = t.OutputTypes[i].Name + found = true + break } } - return TypedStruct{}, false -} -func (a *ABI) FindActionByID(id uint8) (TypedStruct, bool) { - for _, action := range a.Actions { - if action.ID == id { - return action, true - } + if !found { + return nil, false } - return TypedStruct{}, false -} -func (a *ABI) FindOutputByName(name string) (TypedStruct, bool) { - for _, output := range a.Outputs { - if output.Name == name { - return output, true + for _, spec := range t.OutputsSpec { + if spec.Name == typeName { + return spec, true } } - return TypedStruct{}, false + + return nil, false } -func (a *ABI) FindActionByName(name string) (TypedStruct, bool) { - for _, action := range a.Actions { - if action.Name == name { - return action, true +func (t *ABI) GetActionID(name string) (uint8, bool) { + for i := range t.ActionTypes { + if t.ActionTypes[i].Name == name { + return t.ActionTypes[i].ID, true } } - return TypedStruct{}, false + return 0, false } -func (a *ABI) FindTypeByName(name string) (Type, bool) { - for _, typ := range a.Types { - if typ.Name == name { - return typ, true +func (t *ABI) GetOutputID(name string) (uint8, bool) { + for i := range t.OutputTypes { + if t.OutputTypes[i].Name == name { + return t.OutputTypes[i].ID, true } } - return Type{}, false + return 0, false } diff --git a/abi/abi_test.go b/abi/abi_test.go index f918ba0033..04b0ca273c 100644 --- a/abi/abi_test.go +++ b/abi/abi_test.go @@ -4,57 +4,199 @@ package abi import ( + "crypto/sha256" + "encoding/hex" "encoding/json" + "os" + "strings" "testing" + "github.com/StephenButtolph/canoto" "github.com/stretchr/testify/require" + "github.com/ava-labs/hypersdk/chain" + "github.com/ava-labs/hypersdk/chain/chaintest" "github.com/ava-labs/hypersdk/codec" ) -func TestNewABI(t *testing.T) { - require := require.New(t) - - actualABI, err := NewABI([]codec.Typed{ - MockObjectSingleNumber{}, - MockActionTransfer{}, - MockObjectAllNumbers{}, - MockObjectStringAndBytes{}, - MockObjectArrays{}, - MockActionWithTransfer{}, - MockActionWithTransferArray{}, - Outer{}, - ActionWithOutput{}, - FixedBytes{}, - Bools{}, - }, []codec.Typed{ - ActionOutput{}, - }) - require.NoError(err) - - expectedABIJSON := mustReadFile(t, "testdata/abi.json") - expectedABI := mustJSONParse[ABI](t, string(expectedABIJSON)) - - require.Equal(expectedABI, actualABI) +func TestABIHash(t *testing.T) { + r := require.New(t) + + abiJSON := mustReadFile(t, "testdata/abi.json") + abi := &ABI{} + r.NoError(json.Unmarshal(abiJSON, abi)) + + abiBytes := abi.MarshalCanoto() + + abiHash := sha256.Sum256(abiBytes) + expectedHashHex := strings.TrimSpace(string(mustReadFile(t, "testdata/abi.hash.hex"))) + r.Equal(expectedHashHex, hex.EncodeToString(abiHash[:])) +} + +func TestABIFindActionSpecByName(t *testing.T) { + tests := []struct { + name string + actionName string + spec *canoto.Spec + exists bool + }{ + { + name: "registered action", + actionName: "TestAction", + spec: (&chaintest.TestAction{}).CanotoSpec(), + exists: true, + }, + { + name: "unregistered action", + actionName: "NotTestAction", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := require.New(t) + + actionParser := codec.NewCanotoParser[chain.Action]() + outputParser := codec.NewCanotoParser[codec.Typed]() + + r.NoError(actionParser.Register(&chaintest.TestAction{}, chaintest.UnmarshalTestAction)) + + abi := NewABI(actionParser, outputParser) + abi.CalculateCanotoSpec() + + spec, exists := abi.FindActionSpecByName(tt.actionName) + r.Equal(tt.exists, exists) + if exists { + r.Equal(tt.spec, spec) + } + }) + } } -func TestGetABIofABI(t *testing.T) { - require := require.New(t) +func TestABIFindOutputSpecByID(t *testing.T) { + tests := []struct { + name string + outputID uint8 + spec *canoto.Spec + exists bool + }{ + { + name: "registered output", + outputID: (&chaintest.TestOutput{}).GetTypeID(), + spec: (&chaintest.TestOutput{}).CanotoSpec(), + exists: true, + }, + { + name: "unregistered output", + outputID: (&chaintest.TestOutput{}).GetTypeID() + 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := require.New(t) - actualABI, err := NewABI([]codec.Typed{ - ABI{}, - }, []codec.Typed{}) - require.NoError(err) + actionParser := codec.NewCanotoParser[chain.Action]() + outputParser := codec.NewCanotoParser[codec.Typed]() - expectedABIJSON := mustReadFile(t, "testdata/abi.abi.json") - expectedABI := mustJSONParse[ABI](t, string(expectedABIJSON)) + r.NoError(outputParser.Register(&chaintest.TestOutput{}, chaintest.UnmarshalTestOutput)) - require.Equal(expectedABI, actualABI) + abi := NewABI(actionParser, outputParser) + abi.CalculateCanotoSpec() + + spec, exists := abi.FindOutputSpecByID(tt.outputID) + r.Equal(tt.exists, exists) + if exists { + r.Equal(tt.spec, spec) + } + }) + } } -func mustJSONParse[T any](t *testing.T, jsonStr string) T { - var parsed T - err := json.Unmarshal([]byte(jsonStr), &parsed) - require.NoError(t, err, jsonStr) - return parsed +func TestABIGetActionID(t *testing.T) { + tests := []struct { + name string + actionName string + typeID uint8 + exists bool + }{ + { + name: "registered action", + actionName: "TestAction", + typeID: (&chaintest.TestAction{}).GetTypeID(), + exists: true, + }, + { + name: "unregistered action", + actionName: "NotTestAction", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := require.New(t) + + actionParser := codec.NewCanotoParser[chain.Action]() + outputParser := codec.NewCanotoParser[codec.Typed]() + + r.NoError(actionParser.Register(&chaintest.TestAction{}, chaintest.UnmarshalTestAction)) + + abi := NewABI(actionParser, outputParser) + abi.CalculateCanotoSpec() + + actionID, found := abi.GetActionID(tt.actionName) + r.Equal(tt.exists, found) + if tt.exists { + r.Equal(tt.typeID, actionID) + } + }) + } +} + +func TestABIGetOutputID(t *testing.T) { + tests := []struct { + name string + outputName string + typeID uint8 + exists bool + }{ + { + name: "registered output", + outputName: "TestOutput", + typeID: (&chaintest.TestOutput{}).GetTypeID(), + exists: true, + }, + { + name: "unregistered output", + outputName: "NotTestOutput", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := require.New(t) + + actionParser := codec.NewCanotoParser[chain.Action]() + outputParser := codec.NewCanotoParser[codec.Typed]() + + r.NoError(outputParser.Register(&chaintest.TestOutput{}, chaintest.UnmarshalTestOutput)) + + abi := NewABI(actionParser, outputParser) + abi.CalculateCanotoSpec() + + outputID, found := abi.GetOutputID(tt.outputName) + r.Equal(tt.exists, found) + if tt.exists { + r.Equal(tt.typeID, outputID) + } + }) + } +} + +func mustReadFile(t *testing.T, path string) []byte { + t.Helper() + + content, err := os.ReadFile(path) + require.NoError(t, err) + return content } diff --git a/abi/auto_marshal_abi_spec_test.go b/abi/auto_marshal_abi_spec_test.go deleted file mode 100644 index a88df8821d..0000000000 --- a/abi/auto_marshal_abi_spec_test.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package abi - -import ( - "crypto/sha256" - "encoding/hex" - "encoding/json" - "os" - "reflect" - "strings" - "testing" - - "github.com/ava-labs/avalanchego/utils/wrappers" - "github.com/stretchr/testify/require" - - "github.com/ava-labs/hypersdk/codec" - "github.com/ava-labs/hypersdk/consts" -) - -// Sets the expected ABI hash for the testdata/abi.json file -// Used to verify implementation in other languages -func TestABIHash(t *testing.T) { - require := require.New(t) - - // get spec from file - abiJSON := mustReadFile(t, "testdata/abi.json") - abiFromFile := new(ABI) - err := json.Unmarshal(abiJSON, abiFromFile) - require.NoError(err) - - // check hash and compare it to expected - p := &wrappers.Packer{ - Bytes: make([]byte, 0), - MaxSize: consts.NetworkSizeLimit, - } - require.NoError(codec.LinearCodec.MarshalInto(abiFromFile, p)) - abiBytes := p.Bytes - - abiHash := sha256.Sum256(abiBytes) - expectedHashHex := strings.TrimSpace(string(mustReadFile(t, "testdata/abi.hash.hex"))) - require.Equal(expectedHashHex, hex.EncodeToString(abiHash[:])) -} - -// Used to verify implementation in other languages, relies on testdata dir -func TestMarshalSpecs(t *testing.T) { - require := require.New(t) - - testCases := []struct { - name string - object codec.Typed - }{ - {"empty", &MockObjectSingleNumber{}}, - {"uint16", &MockObjectSingleNumber{}}, - {"numbers", &MockObjectAllNumbers{}}, - {"arrays", &MockObjectArrays{}}, - {"transfer", &MockActionTransfer{}}, - {"transferField", &MockActionWithTransfer{}}, - {"transfersArray", &MockActionWithTransferArray{}}, - {"strBytes", &MockObjectStringAndBytes{}}, - {"strByteZero", &MockObjectStringAndBytes{}}, - {"strBytesEmpty", &MockObjectStringAndBytes{}}, - {"strOnly", &MockObjectStringAndBytes{}}, - {"outer", &Outer{}}, - {"fixedBytes", &FixedBytes{}}, - {"bools", &Bools{}}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Create a copy of the original object - unmarshaledFromJSON := reflect.New(reflect.TypeOf(tc.object).Elem()).Interface().(codec.Typed) - unmarshaledFromBytes := reflect.New(reflect.TypeOf(tc.object).Elem()).Interface().(codec.Typed) - - // Get object from file - err := json.Unmarshal(mustReadFile(t, "testdata/"+tc.name+".json"), unmarshaledFromJSON) - require.NoError(err) - - // Marshal the object - objectPacker := codec.NewWriter(0, consts.NetworkSizeLimit) - err = codec.LinearCodec.MarshalInto(unmarshaledFromJSON, objectPacker.Packer) - require.NoError(err) - - objectBytes := objectPacker.Bytes() - - // Compare with expected hex - expectedHex := string(mustReadFile(t, "testdata/"+tc.name+".hex")) - expectedHex = strings.TrimSpace(expectedHex) - require.Equal(expectedHex, hex.EncodeToString(objectBytes)) - - // Unmarshal the object - err = codec.LinearCodec.UnmarshalFrom(&wrappers.Packer{Bytes: objectBytes}, unmarshaledFromBytes) - require.NoError(err) - - // Compare unmarshaled object with the original - require.Equal(unmarshaledFromJSON, unmarshaledFromBytes) - }) - } -} - -func mustReadFile(t *testing.T, path string) []byte { - t.Helper() - - content, err := os.ReadFile(path) - require.NoError(t, err) - return content -} diff --git a/abi/codegen.go b/abi/codegen.go deleted file mode 100644 index 3e1e8f3423..0000000000 --- a/abi/codegen.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package abi - -import ( - "fmt" - "go/format" - "strings" - "unicode" - - "github.com/ava-labs/avalanchego/utils/set" -) - -func GenerateGoStructs(abi ABI, packageName string) (string, error) { - var sb strings.Builder - - sb.WriteString(fmt.Sprintf("package %s\n\n", packageName)) - sb.WriteString("import \"github.com/ava-labs/hypersdk/codec\"\n\n") - - processed := set.Set[string]{} - - for _, typ := range abi.Types { - if processed.Contains(typ.Name) { - continue - } - processed.Add(typ.Name) - - sb.WriteString(fmt.Sprintf("type %s struct {\n", typ.Name)) - for _, field := range typ.Fields { - fieldNameUpperCase := strings.ToUpper(field.Name[0:1]) + field.Name[1:] - - // If the first character is uppercase, use the default JSON tag. - // Otherwise, specify the exported field (upper case) and the lowercase version as the JSON key. - goType := convertToGoType(field.Type) - if unicode.IsUpper(rune(field.Name[0])) { - sb.WriteString(fmt.Sprintf("\t%s %s `serialize:\"true\"`\n", fieldNameUpperCase, goType)) - } else { - sb.WriteString(fmt.Sprintf("\t%s %s `serialize:\"true\" json:\"%s\"`\n", fieldNameUpperCase, goType, field.Name)) - } - } - sb.WriteString("}\n\n") - } - - for _, action := range abi.Actions { - sb.WriteString(fmt.Sprintf("func (%s) GetTypeID() uint8 {\n", action.Name)) - sb.WriteString(fmt.Sprintf("\treturn %d\n", action.ID)) - sb.WriteString("}\n\n") - } - - for _, output := range abi.Outputs { - sb.WriteString(fmt.Sprintf("func (%s) GetTypeID() uint8 {\n", output.Name)) - sb.WriteString(fmt.Sprintf("\treturn %d\n", output.ID)) - sb.WriteString("}\n\n") - } - - formatted, err := format.Source([]byte(sb.String())) - if err != nil { - return "", fmt.Errorf("failed to format generated code: %w", err) - } - - return string(formatted), nil -} - -func convertToGoType(abiType string) string { - switch abiType { - case "string": - return "string" - case "int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64": - return abiType - case "Address": - return "codec.Address" - default: - if strings.HasPrefix(abiType, "[]") { - return "[]" + convertToGoType(strings.TrimPrefix(abiType, "[]")) - } - return abiType // For custom types, we'll use the type name as-is - } -} diff --git a/abi/codegen_test.go b/abi/codegen_test.go deleted file mode 100644 index 02a76e6fc9..0000000000 --- a/abi/codegen_test.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package abi - -import ( - "go/format" - "strings" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestGenerateAllStructs(t *testing.T) { - require := require.New(t) - - abi := mustJSONParse[ABI](t, string(mustReadFile(t, "testdata/abi.json"))) - - code, err := GenerateGoStructs(abi, "abi") - require.NoError(err) - - expected := mustReadFile(t, "mockabi_test.go") - - formatted, err := format.Source(removeCommentLines(expected)) - require.NoError(err) - - require.Equal(string(formatted), code) -} - -func removeCommentLines(input []byte) []byte { - lines := strings.Split(string(input), "\n") - var result []string - for _, line := range lines { - if !strings.HasPrefix(strings.TrimSpace(line), "//") { - result = append(result, line) - } - } - return []byte(strings.Join(result, "\n")) -} diff --git a/abi/dynamic/reflect_marshal.go b/abi/dynamic/reflect_marshal.go deleted file mode 100644 index c0ecb2ef6a..0000000000 --- a/abi/dynamic/reflect_marshal.go +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package dynamic - -import ( - "encoding/json" - "errors" - "fmt" - "reflect" - "regexp" - "strconv" - "strings" - - "github.com/ava-labs/avalanchego/utils/wrappers" - "golang.org/x/text/cases" - "golang.org/x/text/language" - - "github.com/ava-labs/hypersdk/abi" - "github.com/ava-labs/hypersdk/codec" - "github.com/ava-labs/hypersdk/consts" -) - -var ErrTypeNotFound = errors.New("type not found in ABI") - -func Marshal(inputABI abi.ABI, typeName string, jsonData string) ([]byte, error) { - if _, ok := inputABI.FindTypeByName(typeName); !ok { - return nil, fmt.Errorf("marshalling %s: %w", typeName, ErrTypeNotFound) - } - - typeCache := make(map[string]reflect.Type) - - typ, err := getReflectType(typeName, inputABI, typeCache) - if err != nil { - return nil, fmt.Errorf("failed to get reflect type: %w", err) - } - - value := reflect.New(typ).Interface() - - if err := json.Unmarshal([]byte(jsonData), value); err != nil { - return nil, fmt.Errorf("failed to unmarshal JSON data: %w", err) - } - - var typeID byte - found := false - for _, action := range inputABI.Actions { - if action.Name == typeName { - typeID = action.ID - found = true - break - } - } - if !found { - return nil, fmt.Errorf("action %s not found in ABI", typeName) - } - - writer := codec.NewWriter(1, consts.NetworkSizeLimit) - writer.PackByte(typeID) - if err := codec.LinearCodec.MarshalInto(value, writer.Packer); err != nil { - return nil, fmt.Errorf("failed to marshal struct: %w", err) - } - - return writer.Bytes(), nil -} - -func UnmarshalOutput(inputABI abi.ABI, data []byte) (string, error) { - if len(data) == 0 { - return "", nil - } - - typeID := data[0] - outputType, ok := inputABI.FindOutputByID(typeID) - if !ok { - return "", fmt.Errorf("output with id %d not found in ABI", typeID) - } - - return Unmarshal(inputABI, data[1:], outputType.Name) -} - -func UnmarshalAction(inputABI abi.ABI, data []byte) (string, error) { - if len(data) == 0 { - return "", nil - } - - typeID := data[0] - actionType, ok := inputABI.FindActionByID(typeID) - if !ok { - return "", fmt.Errorf("action with id %d not found in ABI", typeID) - } - - return Unmarshal(inputABI, data[1:], actionType.Name) -} - -func Unmarshal(inputABI abi.ABI, data []byte, typeName string) (string, error) { - typeCache := make(map[string]reflect.Type) - - typ, err := getReflectType(typeName, inputABI, typeCache) - if err != nil { - return "", fmt.Errorf("failed to get reflect type: %w", err) - } - - value := reflect.New(typ).Interface() - - packer := wrappers.Packer{ - Bytes: data, - MaxSize: consts.NetworkSizeLimit, - } - if err := codec.LinearCodec.UnmarshalFrom(&packer, value); err != nil { - return "", fmt.Errorf("failed to unmarshal data: %w", err) - } - - jsonData, err := json.Marshal(value) - if err != nil { - return "", fmt.Errorf("failed to marshal struct to JSON: %w", err) - } - - return string(jsonData), nil -} - -// Matches fixed-size arrays like [32]uint8 -var fixedSizeArrayRegex = regexp.MustCompile(`^\[(\d+)\](.+)$`) - -func getReflectType(abiTypeName string, inputABI abi.ABI, typeCache map[string]reflect.Type) (reflect.Type, error) { - switch abiTypeName { - case "string": - return reflect.TypeOf(""), nil - case "uint8": - return reflect.TypeOf(uint8(0)), nil - case "uint16": - return reflect.TypeOf(uint16(0)), nil - case "uint32": - return reflect.TypeOf(uint32(0)), nil - case "uint64": - return reflect.TypeOf(uint64(0)), nil - case "int8": - return reflect.TypeOf(int8(0)), nil - case "int16": - return reflect.TypeOf(int16(0)), nil - case "int32": - return reflect.TypeOf(int32(0)), nil - case "int64": - return reflect.TypeOf(int64(0)), nil - case "Address": - return reflect.TypeOf(codec.Address{}), nil - default: - // golang slices - if strings.HasPrefix(abiTypeName, "[]") { - elemType, err := getReflectType(strings.TrimPrefix(abiTypeName, "[]"), inputABI, typeCache) - if err != nil { - return nil, err - } - return reflect.SliceOf(elemType), nil - } - - // golang arrays - - if match := fixedSizeArrayRegex.FindStringSubmatch(abiTypeName); match != nil { - sizeStr := match[1] - size, err := strconv.Atoi(sizeStr) - if err != nil { - return nil, fmt.Errorf("failed to convert size to int: %w", err) - } - elemType, err := getReflectType(match[2], inputABI, typeCache) - if err != nil { - return nil, err - } - return reflect.ArrayOf(size, elemType), nil - } - - // For custom types, recursively construct the struct type - if cachedType, ok := typeCache[abiTypeName]; ok { - return cachedType, nil - } - - abiType, ok := inputABI.FindTypeByName(abiTypeName) - if !ok { - return nil, fmt.Errorf("type %s not found in ABI", abiTypeName) - } - - // It is a struct, as we don't support anything else as custom types - fields := make([]reflect.StructField, len(abiType.Fields)) - - for i, field := range abiType.Fields { - fieldType, err := getReflectType(field.Type, inputABI, typeCache) - if err != nil { - return nil, err - } - fields[i] = reflect.StructField{ - Name: cases.Title(language.English).String(field.Name), - Type: fieldType, - Tag: reflect.StructTag(fmt.Sprintf(`serialize:"true" json:"%s"`, field.Name)), - } - } - - structType := reflect.StructOf(fields) - typeCache[abiTypeName] = structType - - return structType, nil - } -} diff --git a/abi/dynamic/reflect_marshal_test.go b/abi/dynamic/reflect_marshal_test.go deleted file mode 100644 index 1b0c41a65e..0000000000 --- a/abi/dynamic/reflect_marshal_test.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package dynamic - -import ( - "encoding/hex" - "encoding/json" - "os" - "strings" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/ava-labs/hypersdk/abi" -) - -func TestDynamicMarshal(t *testing.T) { - require := require.New(t) - - abiJSON := mustReadFile(t, "../testdata/abi.json") - var abi abi.ABI - - err := json.Unmarshal(abiJSON, &abi) - require.NoError(err) - - testCases := []struct { - name string - typeName string - }{ - {"empty", "MockObjectSingleNumber"}, - {"uint16", "MockObjectSingleNumber"}, - {"numbers", "MockObjectAllNumbers"}, - {"arrays", "MockObjectArrays"}, - {"transfer", "MockActionTransfer"}, - {"transferField", "MockActionWithTransfer"}, - {"transfersArray", "MockActionWithTransferArray"}, - {"strBytes", "MockObjectStringAndBytes"}, - {"strByteZero", "MockObjectStringAndBytes"}, - {"strBytesEmpty", "MockObjectStringAndBytes"}, - {"strOnly", "MockObjectStringAndBytes"}, - {"outer", "Outer"}, - {"fixedBytes", "FixedBytes"}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Read the JSON data - jsonData := mustReadFile(t, "../testdata/"+tc.name+".json") - - objectBytes, err := Marshal(abi, tc.typeName, string(jsonData)) - require.NoError(err) - - // Compare with expected hex - expectedTypeID, found := abi.FindActionByName(tc.typeName) - require.True(found, "action %s not found in ABI", tc.typeName) - - expectedHex := hex.EncodeToString([]byte{expectedTypeID.ID}) + strings.TrimSpace(string(mustReadFile(t, "../testdata/"+tc.name+".hex"))) - require.Equal(expectedHex, hex.EncodeToString(objectBytes)) - - unmarshaledJSON, err := UnmarshalAction(abi, objectBytes) - require.NoError(err) - - // Compare with expected JSON - require.JSONEq(string(jsonData), unmarshaledJSON) - }) - } -} - -func TestDynamicMarshalErrors(t *testing.T) { - require := require.New(t) - - abiJSON := mustReadFile(t, "../testdata/abi.json") - var abi abi.ABI - - err := json.Unmarshal(abiJSON, &abi) - require.NoError(err) - - // Test malformed JSON - malformedJSON := `{"uint8": 42, "uint16": 1000, "uint32": 100000, "uint64": 10000000000, "int8": -42, "int16": -1000, "int32": -100000, "int64": -10000000000,` - _, err = Marshal(abi, "MockObjectAllNumbers", malformedJSON) - require.Contains(err.Error(), "unexpected end of JSON input") - - // Test wrong struct name - jsonData := mustReadFile(t, "../testdata/numbers.json") - _, err = Marshal(abi, "NonExistentObject", string(jsonData)) - require.ErrorIs(err, ErrTypeNotFound) -} - -func mustReadFile(t *testing.T, path string) []byte { - t.Helper() - - content, err := os.ReadFile(path) - require.NoError(t, err) - return content -} diff --git a/abi/mockabi_test.go b/abi/mockabi_test.go deleted file mode 100644 index 6a5355ec79..0000000000 --- a/abi/mockabi_test.go +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package abi - -import "github.com/ava-labs/hypersdk/codec" - -type MockObjectSingleNumber struct { - Field1 uint16 `serialize:"true"` -} - -type MockActionTransfer struct { - To codec.Address `serialize:"true" json:"to"` - Value uint64 `serialize:"true" json:"value"` - Memo []uint8 `serialize:"true" json:"memo"` -} - -type MockObjectAllNumbers struct { - Uint8 uint8 `serialize:"true" json:"uint8"` - Uint16 uint16 `serialize:"true" json:"uint16"` - Uint32 uint32 `serialize:"true" json:"uint32"` - Uint64 uint64 `serialize:"true" json:"uint64"` - Int8 int8 `serialize:"true" json:"int8"` - Int16 int16 `serialize:"true" json:"int16"` - Int32 int32 `serialize:"true" json:"int32"` - Int64 int64 `serialize:"true" json:"int64"` -} - -type MockObjectStringAndBytes struct { - Field1 string `serialize:"true" json:"field1"` - Field2 []uint8 `serialize:"true" json:"field2"` -} - -type MockObjectArrays struct { - Strings []string `serialize:"true" json:"strings"` - Bytes [][]uint8 `serialize:"true" json:"bytes"` - Uint8s []uint8 `serialize:"true" json:"uint8s"` - Uint16s []uint16 `serialize:"true" json:"uint16s"` - Uint32s []uint32 `serialize:"true" json:"uint32s"` - Uint64s []uint64 `serialize:"true" json:"uint64s"` - Int8s []int8 `serialize:"true" json:"int8s"` - Int16s []int16 `serialize:"true" json:"int16s"` - Int32s []int32 `serialize:"true" json:"int32s"` - Int64s []int64 `serialize:"true" json:"int64s"` -} - -type MockActionWithTransfer struct { - Transfer MockActionTransfer `serialize:"true" json:"transfer"` -} - -type MockActionWithTransferArray struct { - Transfers []MockActionTransfer `serialize:"true" json:"transfers"` -} - -type Outer struct { - Inner Inner `serialize:"true" json:"inner"` - InnerArr []Inner `serialize:"true" json:"innerArr"` -} - -type Inner struct { - Field1 uint8 `serialize:"true" json:"field1"` -} - -type ActionWithOutput struct { - Field1 uint8 `serialize:"true" json:"field1"` -} - -type FixedBytes struct { - TwoBytes [2]uint8 `serialize:"true" json:"twoBytes"` - ThirtyTwoBytes [32]uint8 `serialize:"true" json:"thirtyTwoBytes"` -} - -type Bools struct { - Bool1 bool `serialize:"true" json:"bool1"` - Bool2 bool `serialize:"true" json:"bool2"` - BoolArray []bool `serialize:"true" json:"boolArray"` -} - -type ActionOutput struct { - Field1 uint16 `serialize:"true" json:"field1"` -} - -func (MockObjectSingleNumber) GetTypeID() uint8 { - return 0 -} - -func (MockActionTransfer) GetTypeID() uint8 { - return 1 -} - -func (MockObjectAllNumbers) GetTypeID() uint8 { - return 2 -} - -func (MockObjectStringAndBytes) GetTypeID() uint8 { - return 3 -} - -func (MockObjectArrays) GetTypeID() uint8 { - return 4 -} - -func (MockActionWithTransfer) GetTypeID() uint8 { - return 5 -} - -func (MockActionWithTransferArray) GetTypeID() uint8 { - return 6 -} - -func (Outer) GetTypeID() uint8 { - return 7 -} - -func (ActionWithOutput) GetTypeID() uint8 { - return 8 -} - -func (FixedBytes) GetTypeID() uint8 { - return 9 -} - -func (Bools) GetTypeID() uint8 { - return 10 -} - -func (ActionOutput) GetTypeID() uint8 { - return 0 -} diff --git a/abi/testdata/abi.abi.json b/abi/testdata/abi.abi.json deleted file mode 100644 index a5d3d2d6f7..0000000000 --- a/abi/testdata/abi.abi.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "actions": [ - { - "id": 0, - "name": "ABI" - } - ], - "outputs": [], - "types": [ - { - "name": "ABI", - "fields": [ - { - "name": "actions", - "type": "[]TypedStruct" - }, - { - "name": "outputs", - "type": "[]TypedStruct" - }, - { - "name": "types", - "type": "[]Type" - } - ] - }, - { - "name": "TypedStruct", - "fields": [ - { - "name": "id", - "type": "uint8" - }, - { - "name": "name", - "type": "string" - } - ] - }, - { - "name": "Type", - "fields": [ - { - "name": "name", - "type": "string" - }, - { - "name": "fields", - "type": "[]Field" - } - ] - }, - { - "name": "Field", - "fields": [ - { - "name": "name", - "type": "string" - }, - { - "name": "type", - "type": "string" - } - ] - } - ] -} diff --git a/abi/testdata/abi.hash.hex b/abi/testdata/abi.hash.hex index ebbaaf8b49..91081d8ee1 100644 --- a/abi/testdata/abi.hash.hex +++ b/abi/testdata/abi.hash.hex @@ -1 +1 @@ -3b634237434bc35076e790b52986d735f30d582e9b7b94ad357d91b33072e284 +79a8a4d1a2232df8538cc546c3c17311ae0f37128bf7885769d5051338d80cb6 diff --git a/abi/testdata/abi.json b/abi/testdata/abi.json index 673aefdaf2..76e14ebc1a 100644 --- a/abi/testdata/abi.json +++ b/abi/testdata/abi.json @@ -1,265 +1,88 @@ { - "actions": [ + "actionsSpec": [ { - "id": 0, - "name": "MockObjectSingleNumber" - }, - { - "id": 1, - "name": "MockActionTransfer" - }, - { - "id": 2, - "name": "MockObjectAllNumbers" - }, - { - "id": 3, - "name": "MockObjectStringAndBytes" - }, - { - "id": 4, - "name": "MockObjectArrays" - }, - { - "id": 5, - "name": "MockActionWithTransfer" - }, - { - "id": 6, - "name": "MockActionWithTransferArray" - }, - { - "id": 7, - "name": "Outer" - }, - { - "id": 8, - "name": "ActionWithOutput" - }, - { - "id": 9, - "name": "FixedBytes" - }, - { - "id": 10, - "name": "Bools" - } - ], - "outputs": [ - { - "id": 0, - "name": "ActionOutput" - } - ], - "types": [ - { - "name": "MockObjectSingleNumber", + "name": "TestAction", "fields": [ { - "name": "Field1", - "type": "uint16" - } - ] - }, - { - "name": "MockActionTransfer", - "fields": [ - { - "name": "to", - "type": "Address" - }, - { - "name": "value", - "type": "uint64" + "fieldNumber": 1, + "name": "NumComputeUnits", + "typeUint": 4 }, { - "name": "memo", - "type": "[]uint8" - } - ] - }, - { - "name": "MockObjectAllNumbers", - "fields": [ - { - "name": "uint8", - "type": "uint8" + "fieldNumber": 2, + "name": "SpecifiedStateKeys", + "repeated": true, + "typeString": true }, { - "name": "uint16", - "type": "uint16" + "fieldNumber": 3, + "name": "SpecifiedStateKeyPermissions", + "repeated": true, + "typeUint": 1 }, { - "name": "uint32", - "type": "uint32" + "fieldNumber": 4, + "name": "ReadKeys", + "repeated": true, + "typeBytes": true }, { - "name": "uint64", - "type": "uint64" + "fieldNumber": 5, + "name": "WriteKeys", + "repeated": true, + "typeBytes": true }, { - "name": "int8", - "type": "int8" + "fieldNumber": 6, + "name": "WriteValues", + "repeated": true, + "typeBytes": true }, { - "name": "int16", - "type": "int16" + "fieldNumber": 7, + "name": "ExecuteErr", + "typeBool": true }, { - "name": "int32", - "type": "int32" + "fieldNumber": 8, + "name": "Nonce", + "typeUint": 4 }, { - "name": "int64", - "type": "int64" - } - ] - }, - { - "name": "MockObjectStringAndBytes", - "fields": [ - { - "name": "field1", - "type": "string" + "fieldNumber": 9, + "name": "Start", + "typeInt": 4 }, { - "name": "field2", - "type": "[]uint8" + "fieldNumber": 10, + "name": "End", + "typeInt": 4 } ] - }, - { - "name": "MockObjectArrays", - "fields": [ - { - "name": "strings", - "type": "[]string" - }, - { - "name": "bytes", - "type": "[][]uint8" - }, - { - "name": "uint8s", - "type": "[]uint8" - }, - { - "name": "uint16s", - "type": "[]uint16" - }, - { - "name": "uint32s", - "type": "[]uint32" - }, - { - "name": "uint64s", - "type": "[]uint64" - }, - { - "name": "int8s", - "type": "[]int8" - }, - { - "name": "int16s", - "type": "[]int16" - }, - { - "name": "int32s", - "type": "[]int32" - }, - { - "name": "int64s", - "type": "[]int64" - } - ] - }, - { - "name": "MockActionWithTransfer", - "fields": [ - { - "name": "transfer", - "type": "MockActionTransfer" - } - ] - }, - { - "name": "MockActionWithTransferArray", - "fields": [ - { - "name": "transfers", - "type": "[]MockActionTransfer" - } - ] - }, - { - "name": "Outer", - "fields": [ - { - "name": "inner", - "type": "Inner" - }, - { - "name": "innerArr", - "type": "[]Inner" - } - ] - }, - { - "name": "Inner", - "fields": [ - { - "name": "field1", - "type": "uint8" - } - ] - }, + } + ], + "outputsSpec": [ { - "name": "ActionWithOutput", + "name": "TestOutput", "fields": [ { - "name": "field1", - "type": "uint8" + "fieldNumber": 1, + "name": "Bytes", + "typeBytes": true } ] - }, - { - "name": "FixedBytes", - "fields": [ - { - "name": "twoBytes", - "type": "[2]uint8" - }, - { - "name": "thirtyTwoBytes", - "type": "[32]uint8" - } - ] - }, + } + ], + "actionTypes": [ { - "name": "Bools", - "fields": [ - { - "name": "bool1", - "type": "bool" - }, - { - "name": "bool2", - "type": "bool" - }, - { - "name": "boolArray", - "type": "[]bool" - } - ] - }, + "name": "TestAction", + "id": 0 + } + ], + "outputTypes": [ { - "name": "ActionOutput", - "fields": [ - { - "name": "field1", - "type": "uint16" - } - ] + "name": "TestOutput", + "id": 0 } ] } diff --git a/abi/testdata/arrays.hex b/abi/testdata/arrays.hex deleted file mode 100644 index 8a9b99bd49..0000000000 --- a/abi/testdata/arrays.hex +++ /dev/null @@ -1 +0,0 @@ -00000002000548656c6c6f0005576f726c640000000200000002010200000002030400000002010200000002012c019000000002000111700001388000000002000000012a05f2000000000165a0bc0000000002fffe00000002fed4fe7000000002fffeee90fffec78000000002fffffffed5fa0e00fffffffe9a5f4400 diff --git a/abi/testdata/arrays.json b/abi/testdata/arrays.json deleted file mode 100644 index 81239380a4..0000000000 --- a/abi/testdata/arrays.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "strings": [ - "Hello", - "World" - ], - "bytes": [ - "AQI=", - "AwQ=" - ], - "uint8s": "AQI=", - "uint16s": [ - 300, - 400 - ], - "uint32s": [ - 70000, - 80000 - ], - "uint64s": [ - 5000000000, - 6000000000 - ], - "int8s": [ - -1, - -2 - ], - "int16s": [ - -300, - -400 - ], - "int32s": [ - -70000, - -80000 - ], - "int64s": [ - -5000000000, - -6000000000 - ] -} diff --git a/abi/testdata/bools.hex b/abi/testdata/bools.hex deleted file mode 100644 index 3a2b3cd07a..0000000000 --- a/abi/testdata/bools.hex +++ /dev/null @@ -1 +0,0 @@ -000100000003010001 diff --git a/abi/testdata/bools.json b/abi/testdata/bools.json deleted file mode 100644 index 8102f3d71c..0000000000 --- a/abi/testdata/bools.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "bool1": false, - "bool2": true, - "boolArray": [ - true, - false, - true - ] -} diff --git a/abi/testdata/empty.hex b/abi/testdata/empty.hex deleted file mode 100644 index 739d79706d..0000000000 --- a/abi/testdata/empty.hex +++ /dev/null @@ -1 +0,0 @@ -0000 diff --git a/abi/testdata/empty.json b/abi/testdata/empty.json deleted file mode 100644 index 6044c93d3a..0000000000 --- a/abi/testdata/empty.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "Field1": 0 -} diff --git a/abi/testdata/fixedBytes.hex b/abi/testdata/fixedBytes.hex deleted file mode 100644 index 1a3fba2b01..0000000000 --- a/abi/testdata/fixedBytes.hex +++ /dev/null @@ -1 +0,0 @@ -01020100000000000000000002000000000000000000030000000000000000000400 diff --git a/abi/testdata/fixedBytes.json b/abi/testdata/fixedBytes.json deleted file mode 100644 index 24dc76acdb..0000000000 --- a/abi/testdata/fixedBytes.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "twoBytes": [ - 1, - 2 - ], - "thirtyTwoBytes": [ - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 2, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 4, - 0 - ] -} diff --git a/abi/testdata/numbers.hex b/abi/testdata/numbers.hex deleted file mode 100644 index f66327ddee..0000000000 --- a/abi/testdata/numbers.hex +++ /dev/null @@ -1 +0,0 @@ -fefffefffffffefffffffffffffffe818001800000018000000000000001 diff --git a/abi/testdata/numbers.json b/abi/testdata/numbers.json deleted file mode 100644 index 120342b058..0000000000 --- a/abi/testdata/numbers.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "uint8": 254, - "uint16": 65534, - "uint32": 4294967294, - "uint64": 18446744073709551614, - "int8": -127, - "int16": -32767, - "int32": -2147483647, - "int64": -9223372036854775807 -} diff --git a/abi/testdata/outer.hex b/abi/testdata/outer.hex deleted file mode 100644 index 2632f57d46..0000000000 --- a/abi/testdata/outer.hex +++ /dev/null @@ -1 +0,0 @@ -030000000102 diff --git a/abi/testdata/outer.json b/abi/testdata/outer.json deleted file mode 100644 index 67a14762dd..0000000000 --- a/abi/testdata/outer.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "inner": { - "field1": 3 - }, - "innerArr": [ - { - "field1": 2 - } - ] -} diff --git a/abi/testdata/strByteZero.hex b/abi/testdata/strByteZero.hex deleted file mode 100644 index c3b969c2fb..0000000000 --- a/abi/testdata/strByteZero.hex +++ /dev/null @@ -1 +0,0 @@ -00000000000100 diff --git a/abi/testdata/strByteZero.json b/abi/testdata/strByteZero.json deleted file mode 100644 index 67c3d304b4..0000000000 --- a/abi/testdata/strByteZero.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "field1": "", - "field2": "AA==" -} diff --git a/abi/testdata/strBytes.hex b/abi/testdata/strBytes.hex deleted file mode 100644 index 281080dafa..0000000000 --- a/abi/testdata/strBytes.hex +++ /dev/null @@ -1 +0,0 @@ -000d48656c6c6f2c20576f726c64210000000401020304 diff --git a/abi/testdata/strBytes.json b/abi/testdata/strBytes.json deleted file mode 100644 index 9a6988cd48..0000000000 --- a/abi/testdata/strBytes.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "field1": "Hello, World!", - "field2": "AQIDBA==" -} diff --git a/abi/testdata/strBytesEmpty.hex b/abi/testdata/strBytesEmpty.hex deleted file mode 100644 index 5d946f4625..0000000000 --- a/abi/testdata/strBytesEmpty.hex +++ /dev/null @@ -1 +0,0 @@ -000000000000 diff --git a/abi/testdata/strBytesEmpty.json b/abi/testdata/strBytesEmpty.json deleted file mode 100644 index 2c9e871de4..0000000000 --- a/abi/testdata/strBytesEmpty.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "field1": "", - "field2": "" -} diff --git a/abi/testdata/strOnly.hex b/abi/testdata/strOnly.hex deleted file mode 100644 index 9c25d6724c..0000000000 --- a/abi/testdata/strOnly.hex +++ /dev/null @@ -1 +0,0 @@ -00014100000000 diff --git a/abi/testdata/strOnly.json b/abi/testdata/strOnly.json deleted file mode 100644 index af389fdbc9..0000000000 --- a/abi/testdata/strOnly.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "field1": "A", - "field2": "" -} diff --git a/abi/testdata/transfer.hex b/abi/testdata/transfer.hex deleted file mode 100644 index 2075c8115f..0000000000 --- a/abi/testdata/transfer.hex +++ /dev/null @@ -1 +0,0 @@ -0102030405060708090a0b0c0d0e0f10111213140000000000000000000000000000000000000003e8000000026869 diff --git a/abi/testdata/transfer.json b/abi/testdata/transfer.json deleted file mode 100644 index 06e2ba0265..0000000000 --- a/abi/testdata/transfer.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "to": "0x0102030405060708090a0b0c0d0e0f10111213140000000000000000000000000020db0e6c", - "value": 1000, - "memo": "aGk=" -} diff --git a/abi/testdata/transferField.hex b/abi/testdata/transferField.hex deleted file mode 100644 index 2075c8115f..0000000000 --- a/abi/testdata/transferField.hex +++ /dev/null @@ -1 +0,0 @@ -0102030405060708090a0b0c0d0e0f10111213140000000000000000000000000000000000000003e8000000026869 diff --git a/abi/testdata/transferField.json b/abi/testdata/transferField.json deleted file mode 100644 index fdcb96dcd7..0000000000 --- a/abi/testdata/transferField.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "transfer": { - "to": "0x0102030405060708090a0b0c0d0e0f10111213140000000000000000000000000020db0e6c", - "value": 1000, - "memo": "aGk=" - } -} diff --git a/abi/testdata/transfersArray.hex b/abi/testdata/transfersArray.hex deleted file mode 100644 index dc02117f4e..0000000000 --- a/abi/testdata/transfersArray.hex +++ /dev/null @@ -1 +0,0 @@ -000000020102030405060708090a0b0c0d0e0f10111213140000000000000000000000000000000000000003e80000000268690102030405060708090a0b0c0d0e0f10111213140000000000000000000000000000000000000003e8000000026869 diff --git a/abi/testdata/transfersArray.json b/abi/testdata/transfersArray.json deleted file mode 100644 index 3797c3ba9f..0000000000 --- a/abi/testdata/transfersArray.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "transfers": [ - { - "to": "0x0102030405060708090a0b0c0d0e0f10111213140000000000000000000000000020db0e6c", - "value": 1000, - "memo": "aGk=" - }, - { - "to": "0x0102030405060708090a0b0c0d0e0f10111213140000000000000000000000000020db0e6c", - "value": 1000, - "memo": "aGk=" - } - ] -} diff --git a/abi/testdata/uint16.hex b/abi/testdata/uint16.hex deleted file mode 100644 index ed0bb0f562..0000000000 --- a/abi/testdata/uint16.hex +++ /dev/null @@ -1 +0,0 @@ -302d diff --git a/abi/testdata/uint16.json b/abi/testdata/uint16.json deleted file mode 100644 index 1492a61073..0000000000 --- a/abi/testdata/uint16.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "Field1": 12333 -} diff --git a/api/dependencies.go b/api/dependencies.go index 18666f7393..d30e5641b8 100644 --- a/api/dependencies.go +++ b/api/dependencies.go @@ -28,7 +28,7 @@ type VM interface { Tracer() trace.Tracer Logger() logging.Logger GetParser() chain.Parser - GetABI() abi.ABI + GetABI() *abi.ABI GetRuleFactory() chain.RuleFactory Submit( ctx context.Context, diff --git a/api/indexer/client_test.go b/api/indexer/client_test.go index ac73437f37..9c01d032cf 100644 --- a/api/indexer/client_test.go +++ b/api/indexer/client_test.go @@ -113,8 +113,8 @@ func TestIndexerClientTransactions(t *testing.T) { parser := chaintest.NewTestParser() badParser := &chain.TxTypeParser{ - ActionRegistry: codec.NewTypeParser[chain.Action](), - AuthRegistry: codec.NewTypeParser[chain.Auth](), + ActionParser: codec.NewCanotoParser[chain.Action](), + AuthParser: codec.NewCanotoParser[chain.Auth](), } testCases := []struct { diff --git a/api/jsonrpc/client.go b/api/jsonrpc/client.go index 19550ed824..8fdb0e4053 100644 --- a/api/jsonrpc/client.go +++ b/api/jsonrpc/client.go @@ -114,7 +114,7 @@ func (cli *JSONRPCClient) SubmitTx(ctx context.Context, d []byte) (ids.ID, error return resp.TxID, err } -func (cli *JSONRPCClient) GetABI(ctx context.Context) (abi.ABI, error) { +func (cli *JSONRPCClient) GetABI(ctx context.Context) (*abi.ABI, error) { resp := new(GetABIReply) err := cli.requester.SendRequest( ctx, @@ -122,6 +122,7 @@ func (cli *JSONRPCClient) GetABI(ctx context.Context) (abi.ABI, error) { nil, resp, ) + resp.ABI.CalculateCanotoSpec() return resp.ABI, err } diff --git a/api/jsonrpc/server.go b/api/jsonrpc/server.go index d4cf69327a..c05c51c6fe 100644 --- a/api/jsonrpc/server.go +++ b/api/jsonrpc/server.go @@ -144,7 +144,7 @@ func (j *JSONRPCServer) UnitPrices( type GetABIArgs struct{} type GetABIReply struct { - ABI abi.ABI `json:"abi"` + ABI *abi.ABI `json:"abi"` } func (j *JSONRPCServer) GetABI(_ *http.Request, _ *GetABIArgs, reply *GetABIReply) error { diff --git a/auth/bls.canoto.go b/auth/bls.canoto.go new file mode 100644 index 0000000000..499fe3a3b6 --- /dev/null +++ b/auth/bls.canoto.go @@ -0,0 +1,199 @@ +// Code generated by canoto. DO NOT EDIT. +// versions: +// canoto v0.15.0 +// source: bls.go + +package auth + +import ( + "io" + "reflect" + "sync/atomic" + + "github.com/StephenButtolph/canoto" +) + +// Ensure that unused imports do not error +var ( + _ atomic.Uint64 + + _ = io.ErrUnexpectedEOF +) + +const ( + canoto__BLS__SignerBytes__tag = "\x0a" // canoto.Tag(1, canoto.Len) + canoto__BLS__SignatureBytes__tag = "\x12" // canoto.Tag(2, canoto.Len) +) + +type canotoData_BLS struct { + size atomic.Uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*BLS) CanotoSpec(...reflect.Type) *canoto.Spec { + s := &canoto.Spec{ + Name: "BLS", + Fields: []canoto.FieldType{ + { + FieldNumber: 1, + Name: "SignerBytes", + OneOf: "", + TypeBytes: true, + }, + { + FieldNumber: 2, + Name: "SignatureBytes", + OneOf: "", + TypeBytes: true, + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*BLS) MakeCanoto() *BLS { + return new(BLS) +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *BLS) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a canoto.Reader. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *BLS) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = BLS{} + c.canotoData.size.Store(uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case 1: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadBytes(&r, &c.SignerBytes); err != nil { + return err + } + if len(c.SignerBytes) == 0 { + return canoto.ErrZeroValue + } + case 2: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadBytes(&r, &c.SignatureBytes); err != nil { + return err + } + if len(c.SignatureBytes) == 0 { + return canoto.ErrZeroValue + } + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *BLS) ValidCanoto() bool { + if c == nil { + return true + } + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +func (c *BLS) CalculateCanotoCache() { + if c == nil { + return + } + var size uint64 + if len(c.SignerBytes) != 0 { + size += uint64(len(canoto__BLS__SignerBytes__tag)) + canoto.SizeBytes(c.SignerBytes) + } + if len(c.SignatureBytes) != 0 { + size += uint64(len(canoto__BLS__SignatureBytes__tag)) + canoto.SizeBytes(c.SignatureBytes) + } + c.canotoData.size.Store(size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *BLS) CachedCanotoSize() uint64 { + if c == nil { + return 0 + } + return c.canotoData.size.Load() +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *BLS) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a canoto.Writer and returns the +// resulting canoto.Writer. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *BLS) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if c == nil { + return w + } + if len(c.SignerBytes) != 0 { + canoto.Append(&w, canoto__BLS__SignerBytes__tag) + canoto.AppendBytes(&w, c.SignerBytes) + } + if len(c.SignatureBytes) != 0 { + canoto.Append(&w, canoto__BLS__SignatureBytes__tag) + canoto.AppendBytes(&w, c.SignatureBytes) + } + return w +} diff --git a/auth/bls.go b/auth/bls.go index 1bc6b22207..0d1377464b 100644 --- a/auth/bls.go +++ b/auth/bls.go @@ -3,6 +3,8 @@ package auth +//go:generate go run github.com/StephenButtolph/canoto/canoto $GOFILE + import ( "context" "fmt" @@ -22,10 +24,15 @@ const ( ) type BLS struct { + SignerBytes []byte `canoto:"bytes,1" json:"signerBytes,omitempty"` + SignatureBytes []byte `canoto:"bytes,2" json:"signatureBytes,omitempty"` + Signer *bls.PublicKey `json:"signer,omitempty"` Signature *bls.Signature `json:"signature,omitempty"` addr codec.Address + + canotoData canotoData_BLS } func (b *BLS) address() codec.Address { @@ -63,43 +70,34 @@ func (b *BLS) Sponsor() codec.Address { } func (b *BLS) Bytes() []byte { - bytes := make([]byte, BLSSize) - bytes[0] = BLSID - publicKeyBytes := bls.PublicKeyToBytes(b.Signer) - copy(bytes[1:], publicKeyBytes) - signatureBytes := bls.SignatureToBytes(b.Signature) - copy(bytes[1+bls.PublicKeyLen:], signatureBytes) - return bytes + return append([]byte{BLSID}, b.MarshalCanoto()...) } func UnmarshalBLS(bytes []byte) (chain.Auth, error) { - if len(bytes) != BLSSize { - return nil, fmt.Errorf("invalid BLS auth size %d != %d", len(bytes), BLSSize) - } - if bytes[0] != BLSID { return nil, fmt.Errorf("unexpected BLS typeID: %d != %d", bytes[0], BLSID) } - var b BLS - signer := make([]byte, bls.PublicKeyLen) - copy(signer, bytes[1:]) - signature := make([]byte, bls.SignatureLen) - copy(signature, bytes[1+bls.PublicKeyLen:]) + b := &BLS{} + if err := b.UnmarshalCanoto(bytes[1:]); err != nil { + return nil, err + } - pk, err := bls.PublicKeyFromBytes(signer) + // populate pointer fields + publicKey, err := bls.PublicKeyFromBytes(b.SignerBytes) if err != nil { return nil, err } - b.Signer = pk - sig, err := bls.SignatureFromBytes(signature) + signature, err := bls.SignatureFromBytes(b.SignatureBytes) if err != nil { return nil, err } - b.Signature = sig - return &b, nil + b.Signer = publicKey + b.Signature = signature + + return b, nil } var _ chain.AuthFactory = (*BLSFactory)(nil) @@ -117,7 +115,15 @@ func (b *BLSFactory) Sign(msg []byte) (chain.Auth, error) { if err != nil { return nil, err } - return &BLS{Signer: bls.PublicFromPrivateKey(b.priv), Signature: signature}, nil + + signer := bls.PublicFromPrivateKey(b.priv) + + return &BLS{ + SignerBytes: bls.PublicKeyToBytes(signer), + SignatureBytes: bls.SignatureToBytes(signature), + Signer: signer, + Signature: signature, + }, nil } func (*BLSFactory) MaxUnits() (uint64, uint64) { diff --git a/auth/ed25519.canoto.go b/auth/ed25519.canoto.go new file mode 100644 index 0000000000..df9334825e --- /dev/null +++ b/auth/ed25519.canoto.go @@ -0,0 +1,228 @@ +// Code generated by canoto. DO NOT EDIT. +// versions: +// canoto v0.15.0 +// source: ed25519.go + +package auth + +import ( + "io" + "reflect" + "sync/atomic" + + "github.com/StephenButtolph/canoto" +) + +// Ensure that unused imports do not error +var ( + _ atomic.Uint64 + + _ = io.ErrUnexpectedEOF +) + +const ( + canoto__ED25519__Signer__tag = "\x0a" // canoto.Tag(1, canoto.Len) + canoto__ED25519__Signature__tag = "\x12" // canoto.Tag(2, canoto.Len) +) + +type canotoData_ED25519 struct { + size atomic.Uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*ED25519) CanotoSpec(...reflect.Type) *canoto.Spec { + var zero ED25519 + s := &canoto.Spec{ + Name: "ED25519", + Fields: []canoto.FieldType{ + { + FieldNumber: 1, + Name: "Signer", + OneOf: "", + TypeFixedBytes: uint64(len(zero.Signer)), + }, + { + FieldNumber: 2, + Name: "Signature", + OneOf: "", + TypeFixedBytes: uint64(len(zero.Signature)), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*ED25519) MakeCanoto() *ED25519 { + return new(ED25519) +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *ED25519) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a canoto.Reader. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *ED25519) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = ED25519{} + c.canotoData.size.Store(uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case 1: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + const ( + expectedLength = len(c.Signer) + expectedLengthUint64 = uint64(expectedLength) + ) + var length uint64 + if err := canoto.ReadUint(&r, &length); err != nil { + return err + } + if length != expectedLengthUint64 { + return canoto.ErrInvalidLength + } + if expectedLength > len(r.B) { + return io.ErrUnexpectedEOF + } + + copy((&c.Signer)[:], r.B) + if canoto.IsZero(c.Signer) { + return canoto.ErrZeroValue + } + r.B = r.B[expectedLength:] + case 2: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + const ( + expectedLength = len(c.Signature) + expectedLengthUint64 = uint64(expectedLength) + ) + var length uint64 + if err := canoto.ReadUint(&r, &length); err != nil { + return err + } + if length != expectedLengthUint64 { + return canoto.ErrInvalidLength + } + if expectedLength > len(r.B) { + return io.ErrUnexpectedEOF + } + + copy((&c.Signature)[:], r.B) + if canoto.IsZero(c.Signature) { + return canoto.ErrZeroValue + } + r.B = r.B[expectedLength:] + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *ED25519) ValidCanoto() bool { + if c == nil { + return true + } + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +func (c *ED25519) CalculateCanotoCache() { + if c == nil { + return + } + var size uint64 + if !canoto.IsZero(c.Signer) { + size += uint64(len(canoto__ED25519__Signer__tag)) + canoto.SizeBytes((&c.Signer)[:]) + } + if !canoto.IsZero(c.Signature) { + size += uint64(len(canoto__ED25519__Signature__tag)) + canoto.SizeBytes((&c.Signature)[:]) + } + c.canotoData.size.Store(size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *ED25519) CachedCanotoSize() uint64 { + if c == nil { + return 0 + } + return c.canotoData.size.Load() +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *ED25519) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a canoto.Writer and returns the +// resulting canoto.Writer. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *ED25519) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if c == nil { + return w + } + if !canoto.IsZero(c.Signer) { + canoto.Append(&w, canoto__ED25519__Signer__tag) + canoto.AppendBytes(&w, (&c.Signer)[:]) + } + if !canoto.IsZero(c.Signature) { + canoto.Append(&w, canoto__ED25519__Signature__tag) + canoto.AppendBytes(&w, (&c.Signature)[:]) + } + return w +} diff --git a/auth/ed25519.go b/auth/ed25519.go index 81806fc96a..5b8310da10 100644 --- a/auth/ed25519.go +++ b/auth/ed25519.go @@ -3,6 +3,8 @@ package auth +//go:generate go run github.com/StephenButtolph/canoto/canoto $GOFILE + import ( "context" "fmt" @@ -22,10 +24,12 @@ const ( ) type ED25519 struct { - Signer ed25519.PublicKey `json:"signer"` - Signature ed25519.Signature `json:"signature"` + Signer ed25519.PublicKey `canoto:"fixed bytes,1" json:"signer"` + Signature ed25519.Signature `canoto:"fixed bytes,2" json:"signature"` addr codec.Address + + canotoData canotoData_ED25519 } func (d *ED25519) address() codec.Address { @@ -63,26 +67,19 @@ func (d *ED25519) Sponsor() codec.Address { } func (d *ED25519) Bytes() []byte { - b := make([]byte, ED25519Size) - b[0] = d.GetTypeID() - copy(b[1:], d.Signer[:]) - copy(b[1+ed25519.PublicKeyLen:], d.Signature[:]) - return b + return append([]byte{ED25519ID}, d.MarshalCanoto()...) } func UnmarshalED25519(bytes []byte) (chain.Auth, error) { - if len(bytes) != ED25519Size { - return nil, fmt.Errorf("invalid ed25519 auth size %d != %d", len(bytes), ED25519Size) - } - if bytes[0] != ED25519ID { return nil, fmt.Errorf("unexpected ed25519 typeID: %d != %d", bytes[0], ED25519ID) } - var d ED25519 - copy(d.Signer[:], bytes[1:]) - copy(d.Signature[:], bytes[1+ed25519.PublicKeyLen:]) - return &d, nil + ed25519 := &ED25519{} + if err := ed25519.UnmarshalCanoto(bytes[1:]); err != nil { + return nil, err + } + return ed25519, nil } var _ chain.AuthFactory = (*ED25519Factory)(nil) diff --git a/auth/secp256r1.canoto.go b/auth/secp256r1.canoto.go new file mode 100644 index 0000000000..139afd8918 --- /dev/null +++ b/auth/secp256r1.canoto.go @@ -0,0 +1,228 @@ +// Code generated by canoto. DO NOT EDIT. +// versions: +// canoto v0.15.0 +// source: secp256r1.go + +package auth + +import ( + "io" + "reflect" + "sync/atomic" + + "github.com/StephenButtolph/canoto" +) + +// Ensure that unused imports do not error +var ( + _ atomic.Uint64 + + _ = io.ErrUnexpectedEOF +) + +const ( + canoto__SECP256R1__Signer__tag = "\x0a" // canoto.Tag(1, canoto.Len) + canoto__SECP256R1__Signature__tag = "\x12" // canoto.Tag(2, canoto.Len) +) + +type canotoData_SECP256R1 struct { + size atomic.Uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*SECP256R1) CanotoSpec(...reflect.Type) *canoto.Spec { + var zero SECP256R1 + s := &canoto.Spec{ + Name: "SECP256R1", + Fields: []canoto.FieldType{ + { + FieldNumber: 1, + Name: "Signer", + OneOf: "", + TypeFixedBytes: uint64(len(zero.Signer)), + }, + { + FieldNumber: 2, + Name: "Signature", + OneOf: "", + TypeFixedBytes: uint64(len(zero.Signature)), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*SECP256R1) MakeCanoto() *SECP256R1 { + return new(SECP256R1) +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *SECP256R1) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a canoto.Reader. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *SECP256R1) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = SECP256R1{} + c.canotoData.size.Store(uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case 1: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + const ( + expectedLength = len(c.Signer) + expectedLengthUint64 = uint64(expectedLength) + ) + var length uint64 + if err := canoto.ReadUint(&r, &length); err != nil { + return err + } + if length != expectedLengthUint64 { + return canoto.ErrInvalidLength + } + if expectedLength > len(r.B) { + return io.ErrUnexpectedEOF + } + + copy((&c.Signer)[:], r.B) + if canoto.IsZero(c.Signer) { + return canoto.ErrZeroValue + } + r.B = r.B[expectedLength:] + case 2: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + const ( + expectedLength = len(c.Signature) + expectedLengthUint64 = uint64(expectedLength) + ) + var length uint64 + if err := canoto.ReadUint(&r, &length); err != nil { + return err + } + if length != expectedLengthUint64 { + return canoto.ErrInvalidLength + } + if expectedLength > len(r.B) { + return io.ErrUnexpectedEOF + } + + copy((&c.Signature)[:], r.B) + if canoto.IsZero(c.Signature) { + return canoto.ErrZeroValue + } + r.B = r.B[expectedLength:] + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *SECP256R1) ValidCanoto() bool { + if c == nil { + return true + } + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +func (c *SECP256R1) CalculateCanotoCache() { + if c == nil { + return + } + var size uint64 + if !canoto.IsZero(c.Signer) { + size += uint64(len(canoto__SECP256R1__Signer__tag)) + canoto.SizeBytes((&c.Signer)[:]) + } + if !canoto.IsZero(c.Signature) { + size += uint64(len(canoto__SECP256R1__Signature__tag)) + canoto.SizeBytes((&c.Signature)[:]) + } + c.canotoData.size.Store(size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *SECP256R1) CachedCanotoSize() uint64 { + if c == nil { + return 0 + } + return c.canotoData.size.Load() +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *SECP256R1) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a canoto.Writer and returns the +// resulting canoto.Writer. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *SECP256R1) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if c == nil { + return w + } + if !canoto.IsZero(c.Signer) { + canoto.Append(&w, canoto__SECP256R1__Signer__tag) + canoto.AppendBytes(&w, (&c.Signer)[:]) + } + if !canoto.IsZero(c.Signature) { + canoto.Append(&w, canoto__SECP256R1__Signature__tag) + canoto.AppendBytes(&w, (&c.Signature)[:]) + } + return w +} diff --git a/auth/secp256r1.go b/auth/secp256r1.go index f2a7644fe9..51257d9274 100644 --- a/auth/secp256r1.go +++ b/auth/secp256r1.go @@ -3,6 +3,8 @@ package auth +//go:generate go run github.com/StephenButtolph/canoto/canoto $GOFILE + import ( "context" "fmt" @@ -22,10 +24,12 @@ const ( ) type SECP256R1 struct { - Signer secp256r1.PublicKey `json:"signer"` - Signature secp256r1.Signature `json:"signature"` + Signer secp256r1.PublicKey `canoto:"fixed bytes,1" json:"signer"` + Signature secp256r1.Signature `canoto:"fixed bytes,2" json:"signature"` addr codec.Address + + canotoData canotoData_SECP256R1 } func (d *SECP256R1) address() codec.Address { @@ -63,26 +67,19 @@ func (d *SECP256R1) Sponsor() codec.Address { } func (d *SECP256R1) Bytes() []byte { - b := make([]byte, SECP256R1Size) - b[0] = SECP256R1ID - copy(b[1:], d.Signer[:]) - copy(b[1+secp256r1.PublicKeyLen:], d.Signature[:]) - return b + return append([]byte{d.GetTypeID()}, d.MarshalCanoto()...) } func UnmarshalSECP256R1(bytes []byte) (chain.Auth, error) { - if len(bytes) != SECP256R1Size { - return nil, fmt.Errorf("invalid secp256r1 auth size %d != %d", len(bytes), ED25519Size) - } - if bytes[0] != SECP256R1ID { return nil, fmt.Errorf("unexpected secp256r1 typeID: %d != %d", bytes[0], SECP256R1ID) } - var d SECP256R1 - copy(d.Signer[:], bytes[1:]) - copy(d.Signature[:], bytes[1+secp256r1.PublicKeyLen:]) - return &d, nil + secp256r1 := &SECP256R1{} + if err := secp256r1.UnmarshalCanoto(bytes[1:]); err != nil { + return nil, err + } + return secp256r1, nil } var _ chain.AuthFactory = (*SECP256R1Factory)(nil) diff --git a/chain/chaintest/action.canoto.go b/chain/chaintest/action.canoto.go new file mode 100644 index 0000000000..eb53b238df --- /dev/null +++ b/chain/chaintest/action.canoto.go @@ -0,0 +1,688 @@ +// Code generated by canoto. DO NOT EDIT. +// versions: +// canoto v0.15.0 +// source: action.go + +package chaintest + +import ( + "io" + "reflect" + "sync/atomic" + + "github.com/StephenButtolph/canoto" +) + +// Ensure that unused imports do not error +var ( + _ atomic.Uint64 + + _ = io.ErrUnexpectedEOF +) + +const ( + canoto__TestAction__NumComputeUnits__tag = "\x08" // canoto.Tag(1, canoto.Varint) + canoto__TestAction__SpecifiedStateKeys__tag = "\x12" // canoto.Tag(2, canoto.Len) + canoto__TestAction__SpecifiedStateKeyPermissions__tag = "\x1a" // canoto.Tag(3, canoto.Len) + canoto__TestAction__ReadKeys__tag = "\x22" // canoto.Tag(4, canoto.Len) + canoto__TestAction__WriteKeys__tag = "\x2a" // canoto.Tag(5, canoto.Len) + canoto__TestAction__WriteValues__tag = "\x32" // canoto.Tag(6, canoto.Len) + canoto__TestAction__ExecuteErr__tag = "\x38" // canoto.Tag(7, canoto.Varint) + canoto__TestAction__Nonce__tag = "\x40" // canoto.Tag(8, canoto.Varint) + canoto__TestAction__Start__tag = "\x48" // canoto.Tag(9, canoto.Varint) + canoto__TestAction__End__tag = "\x50" // canoto.Tag(10, canoto.Varint) +) + +type canotoData_TestAction struct { + size atomic.Uint64 + SpecifiedStateKeyPermissionsSize atomic.Uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*TestAction) CanotoSpec(...reflect.Type) *canoto.Spec { + var zero TestAction + s := &canoto.Spec{ + Name: "TestAction", + Fields: []canoto.FieldType{ + { + FieldNumber: 1, + Name: "NumComputeUnits", + OneOf: "", + TypeUint: canoto.SizeOf(zero.NumComputeUnits), + }, + { + FieldNumber: 2, + Name: "SpecifiedStateKeys", + Repeated: true, + OneOf: "", + TypeString: true, + }, + { + FieldNumber: 3, + Name: "SpecifiedStateKeyPermissions", + Repeated: true, + OneOf: "", + TypeUint: canoto.SizeOf(canoto.MakeEntry(zero.SpecifiedStateKeyPermissions)), + }, + { + FieldNumber: 4, + Name: "ReadKeys", + Repeated: true, + OneOf: "", + TypeBytes: true, + }, + { + FieldNumber: 5, + Name: "WriteKeys", + Repeated: true, + OneOf: "", + TypeBytes: true, + }, + { + FieldNumber: 6, + Name: "WriteValues", + Repeated: true, + OneOf: "", + TypeBytes: true, + }, + { + FieldNumber: 7, + Name: "ExecuteErr", + OneOf: "", + TypeBool: true, + }, + { + FieldNumber: 8, + Name: "Nonce", + OneOf: "", + TypeUint: canoto.SizeOf(zero.Nonce), + }, + { + FieldNumber: 9, + Name: "Start", + OneOf: "", + TypeInt: canoto.SizeOf(zero.Start), + }, + { + FieldNumber: 10, + Name: "End", + OneOf: "", + TypeInt: canoto.SizeOf(zero.End), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*TestAction) MakeCanoto() *TestAction { + return new(TestAction) +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *TestAction) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a canoto.Reader. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *TestAction) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = TestAction{} + c.canotoData.size.Store(uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case 1: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.NumComputeUnits); err != nil { + return err + } + if canoto.IsZero(c.NumComputeUnits) { + return canoto.ErrZeroValue + } + case 2: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Skip the first entry because we have already stripped the tag. + remainingBytes := r.B + originalUnsafe := r.Unsafe + r.Unsafe = true + if err := canoto.ReadBytes(&r, new([]byte)); err != nil { + return err + } + r.Unsafe = originalUnsafe + + // Count the number of additional entries after the first entry. + countMinus1, err := canoto.CountBytes(r.B, canoto__TestAction__SpecifiedStateKeys__tag) + if err != nil { + return err + } + c.SpecifiedStateKeys = canoto.MakeSlice(c.SpecifiedStateKeys, countMinus1+1) + + // Read the first entry manually because the tag is still already + // stripped. + r.B = remainingBytes + if err := canoto.ReadString(&r, &c.SpecifiedStateKeys[0]); err != nil { + return err + } + + // Read the rest of the entries, stripping the tag each time. + for i := range countMinus1 { + r.B = r.B[len(canoto__TestAction__SpecifiedStateKeys__tag):] + if err := canoto.ReadString(&r, &c.SpecifiedStateKeys[1+i]); err != nil { + return err + } + } + case 3: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Read the packed field bytes. + originalUnsafe := r.Unsafe + r.Unsafe = true + var msgBytes []byte + if err := canoto.ReadBytes(&r, &msgBytes); err != nil { + return err + } + if len(msgBytes) == 0 { + return canoto.ErrZeroValue + } + r.Unsafe = originalUnsafe + + // Read each value from the packed field bytes into the array. + remainingBytes := r.B + r.B = msgBytes + c.SpecifiedStateKeyPermissions = canoto.MakeSlice(c.SpecifiedStateKeyPermissions, canoto.CountInts(msgBytes)) + for i := range c.SpecifiedStateKeyPermissions { + if err := canoto.ReadUint(&r, &c.SpecifiedStateKeyPermissions[i]); err != nil { + return err + } + } + if canoto.HasNext(&r) { + return canoto.ErrInvalidLength + } + r.B = remainingBytes + c.canotoData.SpecifiedStateKeyPermissionsSize.Store(uint64(len(msgBytes))) + case 4: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Skip the first entry because we have already stripped the tag. + remainingBytes := r.B + originalUnsafe := r.Unsafe + r.Unsafe = true + if err := canoto.ReadBytes(&r, new([]byte)); err != nil { + return err + } + r.Unsafe = originalUnsafe + + // Count the number of additional entries after the first entry. + countMinus1, err := canoto.CountBytes(r.B, canoto__TestAction__ReadKeys__tag) + if err != nil { + return err + } + c.ReadKeys = canoto.MakeSlice(c.ReadKeys, countMinus1+1) + + // Read the first entry manually because the tag is still already + // stripped. + r.B = remainingBytes + if err := canoto.ReadBytes(&r, &c.ReadKeys[0]); err != nil { + return err + } + + // Read the rest of the entries, stripping the tag each time. + for i := range countMinus1 { + r.B = r.B[len(canoto__TestAction__ReadKeys__tag):] + if err := canoto.ReadBytes(&r, &c.ReadKeys[1+i]); err != nil { + return err + } + } + case 5: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Skip the first entry because we have already stripped the tag. + remainingBytes := r.B + originalUnsafe := r.Unsafe + r.Unsafe = true + if err := canoto.ReadBytes(&r, new([]byte)); err != nil { + return err + } + r.Unsafe = originalUnsafe + + // Count the number of additional entries after the first entry. + countMinus1, err := canoto.CountBytes(r.B, canoto__TestAction__WriteKeys__tag) + if err != nil { + return err + } + c.WriteKeys = canoto.MakeSlice(c.WriteKeys, countMinus1+1) + + // Read the first entry manually because the tag is still already + // stripped. + r.B = remainingBytes + if err := canoto.ReadBytes(&r, &c.WriteKeys[0]); err != nil { + return err + } + + // Read the rest of the entries, stripping the tag each time. + for i := range countMinus1 { + r.B = r.B[len(canoto__TestAction__WriteKeys__tag):] + if err := canoto.ReadBytes(&r, &c.WriteKeys[1+i]); err != nil { + return err + } + } + case 6: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + // Skip the first entry because we have already stripped the tag. + remainingBytes := r.B + originalUnsafe := r.Unsafe + r.Unsafe = true + if err := canoto.ReadBytes(&r, new([]byte)); err != nil { + return err + } + r.Unsafe = originalUnsafe + + // Count the number of additional entries after the first entry. + countMinus1, err := canoto.CountBytes(r.B, canoto__TestAction__WriteValues__tag) + if err != nil { + return err + } + c.WriteValues = canoto.MakeSlice(c.WriteValues, countMinus1+1) + + // Read the first entry manually because the tag is still already + // stripped. + r.B = remainingBytes + if err := canoto.ReadBytes(&r, &c.WriteValues[0]); err != nil { + return err + } + + // Read the rest of the entries, stripping the tag each time. + for i := range countMinus1 { + r.B = r.B[len(canoto__TestAction__WriteValues__tag):] + if err := canoto.ReadBytes(&r, &c.WriteValues[1+i]); err != nil { + return err + } + } + case 7: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadBool(&r, &c.ExecuteErr); err != nil { + return err + } + if canoto.IsZero(c.ExecuteErr) { + return canoto.ErrZeroValue + } + case 8: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.Nonce); err != nil { + return err + } + if canoto.IsZero(c.Nonce) { + return canoto.ErrZeroValue + } + case 9: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadInt(&r, &c.Start); err != nil { + return err + } + if canoto.IsZero(c.Start) { + return canoto.ErrZeroValue + } + case 10: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadInt(&r, &c.End); err != nil { + return err + } + if canoto.IsZero(c.End) { + return canoto.ErrZeroValue + } + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *TestAction) ValidCanoto() bool { + if c == nil { + return true + } + for _, v := range c.SpecifiedStateKeys { + if !canoto.ValidString(v) { + return false + } + } + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +func (c *TestAction) CalculateCanotoCache() { + if c == nil { + return + } + var size uint64 + if !canoto.IsZero(c.NumComputeUnits) { + size += uint64(len(canoto__TestAction__NumComputeUnits__tag)) + canoto.SizeUint(c.NumComputeUnits) + } + for _, v := range c.SpecifiedStateKeys { + size += uint64(len(canoto__TestAction__SpecifiedStateKeys__tag)) + canoto.SizeBytes(v) + } + if len(c.SpecifiedStateKeyPermissions) != 0 { + var fieldSize uint64 + for _, v := range c.SpecifiedStateKeyPermissions { + fieldSize += canoto.SizeUint(v) + } + size += uint64(len(canoto__TestAction__SpecifiedStateKeyPermissions__tag)) + canoto.SizeUint(fieldSize) + fieldSize + c.canotoData.SpecifiedStateKeyPermissionsSize.Store(fieldSize) + } + for _, v := range c.ReadKeys { + size += uint64(len(canoto__TestAction__ReadKeys__tag)) + canoto.SizeBytes(v) + } + for _, v := range c.WriteKeys { + size += uint64(len(canoto__TestAction__WriteKeys__tag)) + canoto.SizeBytes(v) + } + for _, v := range c.WriteValues { + size += uint64(len(canoto__TestAction__WriteValues__tag)) + canoto.SizeBytes(v) + } + if !canoto.IsZero(c.ExecuteErr) { + size += uint64(len(canoto__TestAction__ExecuteErr__tag)) + canoto.SizeBool + } + if !canoto.IsZero(c.Nonce) { + size += uint64(len(canoto__TestAction__Nonce__tag)) + canoto.SizeUint(c.Nonce) + } + if !canoto.IsZero(c.Start) { + size += uint64(len(canoto__TestAction__Start__tag)) + canoto.SizeInt(c.Start) + } + if !canoto.IsZero(c.End) { + size += uint64(len(canoto__TestAction__End__tag)) + canoto.SizeInt(c.End) + } + c.canotoData.size.Store(size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *TestAction) CachedCanotoSize() uint64 { + if c == nil { + return 0 + } + return c.canotoData.size.Load() +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *TestAction) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a canoto.Writer and returns the +// resulting canoto.Writer. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *TestAction) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if c == nil { + return w + } + if !canoto.IsZero(c.NumComputeUnits) { + canoto.Append(&w, canoto__TestAction__NumComputeUnits__tag) + canoto.AppendUint(&w, c.NumComputeUnits) + } + for _, v := range c.SpecifiedStateKeys { + canoto.Append(&w, canoto__TestAction__SpecifiedStateKeys__tag) + canoto.AppendBytes(&w, v) + } + if len(c.SpecifiedStateKeyPermissions) != 0 { + canoto.Append(&w, canoto__TestAction__SpecifiedStateKeyPermissions__tag) + canoto.AppendUint(&w, c.canotoData.SpecifiedStateKeyPermissionsSize.Load()) + for _, v := range c.SpecifiedStateKeyPermissions { + canoto.AppendUint(&w, v) + } + } + for _, v := range c.ReadKeys { + canoto.Append(&w, canoto__TestAction__ReadKeys__tag) + canoto.AppendBytes(&w, v) + } + for _, v := range c.WriteKeys { + canoto.Append(&w, canoto__TestAction__WriteKeys__tag) + canoto.AppendBytes(&w, v) + } + for _, v := range c.WriteValues { + canoto.Append(&w, canoto__TestAction__WriteValues__tag) + canoto.AppendBytes(&w, v) + } + if !canoto.IsZero(c.ExecuteErr) { + canoto.Append(&w, canoto__TestAction__ExecuteErr__tag) + canoto.AppendBool(&w, true) + } + if !canoto.IsZero(c.Nonce) { + canoto.Append(&w, canoto__TestAction__Nonce__tag) + canoto.AppendUint(&w, c.Nonce) + } + if !canoto.IsZero(c.Start) { + canoto.Append(&w, canoto__TestAction__Start__tag) + canoto.AppendInt(&w, c.Start) + } + if !canoto.IsZero(c.End) { + canoto.Append(&w, canoto__TestAction__End__tag) + canoto.AppendInt(&w, c.End) + } + return w +} + +const ( + canoto__TestOutput__Bytes__tag = "\x0a" // canoto.Tag(1, canoto.Len) +) + +type canotoData_TestOutput struct { + size atomic.Uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*TestOutput) CanotoSpec(...reflect.Type) *canoto.Spec { + s := &canoto.Spec{ + Name: "TestOutput", + Fields: []canoto.FieldType{ + { + FieldNumber: 1, + Name: "Bytes", + OneOf: "", + TypeBytes: true, + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*TestOutput) MakeCanoto() *TestOutput { + return new(TestOutput) +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *TestOutput) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a canoto.Reader. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *TestOutput) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = TestOutput{} + c.canotoData.size.Store(uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case 1: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadBytes(&r, &c.Bytes); err != nil { + return err + } + if len(c.Bytes) == 0 { + return canoto.ErrZeroValue + } + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *TestOutput) ValidCanoto() bool { + if c == nil { + return true + } + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +func (c *TestOutput) CalculateCanotoCache() { + if c == nil { + return + } + var size uint64 + if len(c.Bytes) != 0 { + size += uint64(len(canoto__TestOutput__Bytes__tag)) + canoto.SizeBytes(c.Bytes) + } + c.canotoData.size.Store(size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *TestOutput) CachedCanotoSize() uint64 { + if c == nil { + return 0 + } + return c.canotoData.size.Load() +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *TestOutput) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a canoto.Writer and returns the +// resulting canoto.Writer. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *TestOutput) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if c == nil { + return w + } + if len(c.Bytes) != 0 { + canoto.Append(&w, canoto__TestOutput__Bytes__tag) + canoto.AppendBytes(&w, c.Bytes) + } + return w +} diff --git a/chain/chaintest/action.go b/chain/chaintest/action.go index f67aabab39..7571815e46 100644 --- a/chain/chaintest/action.go +++ b/chain/chaintest/action.go @@ -3,6 +3,8 @@ package chaintest +//go:generate go run github.com/StephenButtolph/canoto/canoto $GOFILE + import ( "bytes" "context" @@ -11,12 +13,10 @@ import ( "testing" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils/wrappers" "github.com/stretchr/testify/require" "github.com/ava-labs/hypersdk/chain" "github.com/ava-labs/hypersdk/codec" - "github.com/ava-labs/hypersdk/consts" "github.com/ava-labs/hypersdk/state" ) @@ -30,16 +30,18 @@ var ( ) type TestAction struct { - NumComputeUnits uint64 `serialize:"true" json:"computeUnits"` - SpecifiedStateKeys []string `serialize:"true" json:"specifiedStateKeys"` - SpecifiedStateKeyPermissions []state.Permissions `serialize:"true" json:"specifiedStateKeyPermissions"` - ReadKeys [][]byte `serialize:"true" json:"reads"` - WriteKeys [][]byte `serialize:"true" json:"writeKeys"` - WriteValues [][]byte `serialize:"true" json:"writeValues"` - ExecuteErr bool `serialize:"true" json:"executeErr"` - Nonce uint64 `serialize:"true" json:"nonce"` - Start int64 `serialize:"true" json:"start"` - End int64 `serialize:"true" json:"end"` + NumComputeUnits uint64 `canoto:"uint,1" json:"computeUnits"` + SpecifiedStateKeys []string `canoto:"repeated string,2" json:"specifiedStateKeys"` + SpecifiedStateKeyPermissions []state.Permissions `canoto:"repeated uint,3" json:"specifiedStateKeyPermissions"` + ReadKeys [][]byte `canoto:"repeated bytes,4" json:"reads"` + WriteKeys [][]byte `canoto:"repeated bytes,5" json:"writeKeys"` + WriteValues [][]byte `canoto:"repeated bytes,6" json:"writeValues"` + ExecuteErr bool `canoto:"bool,7" json:"executeErr"` + Nonce uint64 `canoto:"uint,8" json:"nonce"` + Start int64 `canoto:"int,9" json:"start"` + End int64 `canoto:"int,10" json:"end"` + + canotoData canotoData_TestAction } // NewDummyTestAction returns a single valid instance of TestAction @@ -55,15 +57,10 @@ func NewDummyTestActions(numActions int) []*TestAction { actions := make([]*TestAction, numActions) for i := 0; i < numActions; i++ { actions[i] = &TestAction{ - NumComputeUnits: 1, - SpecifiedStateKeys: []string{}, - SpecifiedStateKeyPermissions: []state.Permissions{}, - ReadKeys: [][]byte{}, - WriteKeys: [][]byte{}, - WriteValues: [][]byte{}, - Start: -1, - End: -1, - Nonce: uint64(i), + NumComputeUnits: 1, + Start: -1, + End: -1, + Nonce: uint64(i), } } @@ -75,22 +72,7 @@ func (*TestAction) GetTypeID() uint8 { } func (t *TestAction) Bytes() []byte { - p := &wrappers.Packer{ - Bytes: make([]byte, 0, 4096), - MaxSize: consts.NetworkSizeLimit, - } - p.PackByte(t.GetTypeID()) - // XXX: AvalancheGo codec should never error for a valid value. Running e2e, we only - // interact with values unmarshalled from the network, which should guarantee a valid - // value here. - // Panic if we fail to marshal a value here to catch any potential bugs early. - // TODO: complete migration of user defined types to Canoto, so we do not need a panic - // here. - err := codec.LinearCodec.MarshalInto(t, p) - if err != nil { - panic(err) - } - return p.Bytes + return append([]byte{t.GetTypeID()}, t.MarshalCanoto()...) } func UnmarshalTestAction(b []byte) (chain.Action, error) { @@ -102,12 +84,10 @@ func UnmarshalTestAction(b []byte) (chain.Action, error) { return nil, fmt.Errorf("unexpected test action typeID: %d != %d", b[0], TestActionID) } - if err := codec.LinearCodec.UnmarshalFrom( - &wrappers.Packer{Bytes: b[1:]}, - t, - ); err != nil { + if err := t.UnmarshalCanoto(b[1:]); err != nil { return nil, err } + return t, nil } @@ -156,17 +136,26 @@ func (t *TestAction) ValidRange(_ chain.Rules) (int64, int64) { return t.Start, t.End } -type TestOutput struct{} +type TestOutput struct { + Bytes []byte `canoto:"bytes,1" json:"bytes"` + + canotoData canotoData_TestOutput +} func (*TestOutput) GetTypeID() uint8 { return TestActionID } func UnmarshalTestOutput(b []byte) (codec.Typed, error) { + t := &TestOutput{} if !bytes.Equal([]byte{TestActionID}, b) { return nil, fmt.Errorf("expected lone typeID %d for test output, found %x", 0, b) } - return &TestOutput{}, nil + if err := t.UnmarshalCanoto(b[1:]); err != nil { + return nil, err + } + + return t, nil } // ActionTest is a single parameterized test. It calls Execute on the action with the passed parameters diff --git a/chain/chaintest/auth.canoto.go b/chain/chaintest/auth.canoto.go new file mode 100644 index 0000000000..8555df550d --- /dev/null +++ b/chain/chaintest/auth.canoto.go @@ -0,0 +1,328 @@ +// Code generated by canoto. DO NOT EDIT. +// versions: +// canoto v0.15.0 +// source: auth.go + +package chaintest + +import ( + "io" + "reflect" + "sync/atomic" + + "github.com/StephenButtolph/canoto" +) + +// Ensure that unused imports do not error +var ( + _ atomic.Uint64 + + _ = io.ErrUnexpectedEOF +) + +const ( + canoto__TestAuth__NumComputeUnits__tag = "\x08" // canoto.Tag(1, canoto.Varint) + canoto__TestAuth__ActorAddress__tag = "\x12" // canoto.Tag(2, canoto.Len) + canoto__TestAuth__SponsorAddress__tag = "\x1a" // canoto.Tag(3, canoto.Len) + canoto__TestAuth__ShouldErr__tag = "\x20" // canoto.Tag(4, canoto.Varint) + canoto__TestAuth__Start__tag = "\x28" // canoto.Tag(5, canoto.Varint) + canoto__TestAuth__End__tag = "\x30" // canoto.Tag(6, canoto.Varint) +) + +type canotoData_TestAuth struct { + size atomic.Uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*TestAuth) CanotoSpec(...reflect.Type) *canoto.Spec { + var zero TestAuth + s := &canoto.Spec{ + Name: "TestAuth", + Fields: []canoto.FieldType{ + { + FieldNumber: 1, + Name: "NumComputeUnits", + OneOf: "", + TypeUint: canoto.SizeOf(zero.NumComputeUnits), + }, + { + FieldNumber: 2, + Name: "ActorAddress", + OneOf: "", + TypeFixedBytes: uint64(len(zero.ActorAddress)), + }, + { + FieldNumber: 3, + Name: "SponsorAddress", + OneOf: "", + TypeFixedBytes: uint64(len(zero.SponsorAddress)), + }, + { + FieldNumber: 4, + Name: "ShouldErr", + OneOf: "", + TypeBool: true, + }, + { + FieldNumber: 5, + Name: "Start", + OneOf: "", + TypeInt: canoto.SizeOf(zero.Start), + }, + { + FieldNumber: 6, + Name: "End", + OneOf: "", + TypeInt: canoto.SizeOf(zero.End), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*TestAuth) MakeCanoto() *TestAuth { + return new(TestAuth) +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *TestAuth) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a canoto.Reader. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *TestAuth) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = TestAuth{} + c.canotoData.size.Store(uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case 1: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.NumComputeUnits); err != nil { + return err + } + if canoto.IsZero(c.NumComputeUnits) { + return canoto.ErrZeroValue + } + case 2: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + const ( + expectedLength = len(c.ActorAddress) + expectedLengthUint64 = uint64(expectedLength) + ) + var length uint64 + if err := canoto.ReadUint(&r, &length); err != nil { + return err + } + if length != expectedLengthUint64 { + return canoto.ErrInvalidLength + } + if expectedLength > len(r.B) { + return io.ErrUnexpectedEOF + } + + copy((&c.ActorAddress)[:], r.B) + if canoto.IsZero(c.ActorAddress) { + return canoto.ErrZeroValue + } + r.B = r.B[expectedLength:] + case 3: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + const ( + expectedLength = len(c.SponsorAddress) + expectedLengthUint64 = uint64(expectedLength) + ) + var length uint64 + if err := canoto.ReadUint(&r, &length); err != nil { + return err + } + if length != expectedLengthUint64 { + return canoto.ErrInvalidLength + } + if expectedLength > len(r.B) { + return io.ErrUnexpectedEOF + } + + copy((&c.SponsorAddress)[:], r.B) + if canoto.IsZero(c.SponsorAddress) { + return canoto.ErrZeroValue + } + r.B = r.B[expectedLength:] + case 4: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadBool(&r, &c.ShouldErr); err != nil { + return err + } + if canoto.IsZero(c.ShouldErr) { + return canoto.ErrZeroValue + } + case 5: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadInt(&r, &c.Start); err != nil { + return err + } + if canoto.IsZero(c.Start) { + return canoto.ErrZeroValue + } + case 6: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadInt(&r, &c.End); err != nil { + return err + } + if canoto.IsZero(c.End) { + return canoto.ErrZeroValue + } + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *TestAuth) ValidCanoto() bool { + if c == nil { + return true + } + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +func (c *TestAuth) CalculateCanotoCache() { + if c == nil { + return + } + var size uint64 + if !canoto.IsZero(c.NumComputeUnits) { + size += uint64(len(canoto__TestAuth__NumComputeUnits__tag)) + canoto.SizeUint(c.NumComputeUnits) + } + if !canoto.IsZero(c.ActorAddress) { + size += uint64(len(canoto__TestAuth__ActorAddress__tag)) + canoto.SizeBytes((&c.ActorAddress)[:]) + } + if !canoto.IsZero(c.SponsorAddress) { + size += uint64(len(canoto__TestAuth__SponsorAddress__tag)) + canoto.SizeBytes((&c.SponsorAddress)[:]) + } + if !canoto.IsZero(c.ShouldErr) { + size += uint64(len(canoto__TestAuth__ShouldErr__tag)) + canoto.SizeBool + } + if !canoto.IsZero(c.Start) { + size += uint64(len(canoto__TestAuth__Start__tag)) + canoto.SizeInt(c.Start) + } + if !canoto.IsZero(c.End) { + size += uint64(len(canoto__TestAuth__End__tag)) + canoto.SizeInt(c.End) + } + c.canotoData.size.Store(size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *TestAuth) CachedCanotoSize() uint64 { + if c == nil { + return 0 + } + return c.canotoData.size.Load() +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *TestAuth) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a canoto.Writer and returns the +// resulting canoto.Writer. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *TestAuth) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if c == nil { + return w + } + if !canoto.IsZero(c.NumComputeUnits) { + canoto.Append(&w, canoto__TestAuth__NumComputeUnits__tag) + canoto.AppendUint(&w, c.NumComputeUnits) + } + if !canoto.IsZero(c.ActorAddress) { + canoto.Append(&w, canoto__TestAuth__ActorAddress__tag) + canoto.AppendBytes(&w, (&c.ActorAddress)[:]) + } + if !canoto.IsZero(c.SponsorAddress) { + canoto.Append(&w, canoto__TestAuth__SponsorAddress__tag) + canoto.AppendBytes(&w, (&c.SponsorAddress)[:]) + } + if !canoto.IsZero(c.ShouldErr) { + canoto.Append(&w, canoto__TestAuth__ShouldErr__tag) + canoto.AppendBool(&w, true) + } + if !canoto.IsZero(c.Start) { + canoto.Append(&w, canoto__TestAuth__Start__tag) + canoto.AppendInt(&w, c.Start) + } + if !canoto.IsZero(c.End) { + canoto.Append(&w, canoto__TestAuth__End__tag) + canoto.AppendInt(&w, c.End) + } + return w +} diff --git a/chain/chaintest/auth.go b/chain/chaintest/auth.go index e01b98db63..4c16e97179 100644 --- a/chain/chaintest/auth.go +++ b/chain/chaintest/auth.go @@ -3,13 +3,13 @@ package chaintest +//go:generate go run github.com/StephenButtolph/canoto/canoto $GOFILE + import ( "context" "errors" "fmt" - "github.com/ava-labs/avalanchego/utils/wrappers" - "github.com/ava-labs/hypersdk/chain" "github.com/ava-labs/hypersdk/codec" ) @@ -23,12 +23,14 @@ var ( ) type TestAuth struct { - NumComputeUnits uint64 `serialize:"true" json:"numComputeUnits"` - ActorAddress codec.Address `serialize:"true" json:"actor"` - SponsorAddress codec.Address `serialize:"true" json:"sponsor"` - ShouldErr bool `serialize:"true" json:"shouldErr"` - Start int64 `serialize:"true" json:"start"` - End int64 `serialize:"true" json:"end"` + NumComputeUnits uint64 `canoto:"uint,1" json:"numComputeUnits"` + ActorAddress codec.Address `canoto:"fixed bytes,2" json:"actor"` + SponsorAddress codec.Address `canoto:"fixed bytes,3" json:"sponsor"` + ShouldErr bool `canoto:"bool,4" json:"shouldErr"` + Start int64 `canoto:"int,5" json:"start"` + End int64 `canoto:"int,6" json:"end"` + + canotoData canotoData_TestAuth } func NewDummyTestAuth() *TestAuth { @@ -46,19 +48,7 @@ func (*TestAuth) GetTypeID() uint8 { } func (t *TestAuth) Bytes() []byte { - p := &wrappers.Packer{ - Bytes: make([]byte, 0, 256), - MaxSize: 256, - } - p.PackByte(t.GetTypeID()) - // XXX: AvalancheGo codec should never error for a valid value. Running e2e, we only - // interact with values unmarshalled from the network, which should guarantee a valid - // value here. - // Panic if we fail to marshal a value here to catch any potential bugs early. - // TODO: complete migration of user defined types to Canoto, so we do not need a panic - // here. - _ = codec.LinearCodec.MarshalInto(t, p) - return p.Bytes + return append([]byte{t.GetTypeID()}, t.MarshalCanoto()...) } func UnmarshalTestAuth(bytes []byte) (chain.Auth, error) { @@ -68,10 +58,7 @@ func UnmarshalTestAuth(bytes []byte) (chain.Auth, error) { return nil, fmt.Errorf("unexpected test auth typeID: %d != %d", bytes[0], TestAuthTypeID) } - if err := codec.LinearCodec.UnmarshalFrom( - &wrappers.Packer{Bytes: bytes[1:]}, - t, - ); err != nil { + if err := t.UnmarshalCanoto(bytes[1:]); err != nil { return nil, err } return t, nil diff --git a/chain/chaintest/parser.go b/chain/chaintest/parser.go index adee4420ff..3d170628f5 100644 --- a/chain/chaintest/parser.go +++ b/chain/chaintest/parser.go @@ -11,21 +11,16 @@ import ( ) func NewTestParser() *chain.TxTypeParser { - actionCodec := codec.NewTypeParser[chain.Action]() - authCodec := codec.NewTypeParser[chain.Auth]() - outputCodec := codec.NewTypeParser[codec.Typed]() + actionCodec := codec.NewCanotoParser[chain.Action]() + authCodec := codec.NewCanotoParser[chain.Auth]() err := errors.Join( actionCodec.Register(&TestAction{}, UnmarshalTestAction), authCodec.Register(&TestAuth{}, UnmarshalTestAuth), - outputCodec.Register(&TestOutput{}, UnmarshalTestOutput), ) if err != nil { panic(err) } - return &chain.TxTypeParser{ - ActionRegistry: actionCodec, - AuthRegistry: authCodec, - } + return chain.NewTxTypeParser(actionCodec, authCodec) } diff --git a/chain/stateless_block_test.go b/chain/stateless_block_test.go index 633ce7d872..b7fcff43d1 100644 --- a/chain/stateless_block_test.go +++ b/chain/stateless_block_test.go @@ -107,7 +107,7 @@ func TestParseHardcodedBlock(t *testing.T) { r := require.New(t) // Hardcoded bytes of the block to verify there are no unexpected serialization changes - blockHex := "0a20e902a9a86640bfdb1cd0e36c0cc982b83e5765fad5f6bbe6abdcce7b5ae7d7c7117b000000000000001901000000000000002abd010a2508d00f12203d0ad12b8ee8928edf248ca91ca55600fb383f07c32bff1d6dec472b25cf59a712360000000000000000010000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffff1a5c00000000000000000101020300000000000000000000000000000000000000000000000000000000000001020300000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffff2ac6010a2e08d00f12203d0ad12b8ee8928edf248ca91ca55600fb383f07c32bff1d6dec472b25cf59a719010000000000000012360000000000000000010000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffff1a5c00000000000000000101020300000000000000000000000000000000000000000000000000000000000001020300000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffff2ac6010a2e08d00f12203d0ad12b8ee8928edf248ca91ca55600fb383f07c32bff1d6dec472b25cf59a719020000000000000012360000000000000000010000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffff1a5c00000000000000000101020300000000000000000000000000000000000000000000000000000000000001020300000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffff32204a177205df5c29929d06db9d941f83d5ea985de302015e99252d16469a6610db" + blockHex := "0a20e902a9a86640bfdb1cd0e36c0cc982b83e5765fad5f6bbe6abdcce7b5ae7d7c7117b000000000000001901000000000000002a7f0a2508d00f12203d0ad12b8ee8928edf248ca91ca55600fb383f07c32bff1d6dec472b25cf59a71207000801480150011a4d00080112210102030000000000000000000000000000000000000000000000000000000000001a21010203000000000000000000000000000000000000000000000000000000000000280130012a88010a2e08d00f12203d0ad12b8ee8928edf248ca91ca55600fb383f07c32bff1d6dec472b25cf59a71901000000000000001207000801480150011a4d00080112210102030000000000000000000000000000000000000000000000000000000000001a21010203000000000000000000000000000000000000000000000000000000000000280130012a88010a2e08d00f12203d0ad12b8ee8928edf248ca91ca55600fb383f07c32bff1d6dec472b25cf59a71902000000000000001207000801480150011a4d00080112210102030000000000000000000000000000000000000000000000000000000000001a210102030000000000000000000000000000000000000000000000000000000000002801300132204a177205df5c29929d06db9d941f83d5ea985de302015e99252d16469a6610db" hardcodedBlockBytes, err := hex.DecodeString(blockHex) r.NoError(err) @@ -170,12 +170,12 @@ func TestBlockWithNilTransaction(t *testing.T) { // goos: darwin // goarch: arm64 // pkg: github.com/ava-labs/hypersdk/chain -// BenchmarkUnmarshalBlock/UnmarshalBlock-0-txs-12 1000000 1148 ns/op 192 B/op 1 allocs/op -// BenchmarkUnmarshalBlock/UnmarshalBlock-1-txs-12 350851 3515 ns/op 2600 B/op 24 allocs/op -// BenchmarkUnmarshalBlock/UnmarshalBlock-10-txs-12 54644 23394 ns/op 24272 B/op 222 allocs/op -// BenchmarkUnmarshalBlock/UnmarshalBlock-1000-txs-12 546 2143920 ns/op 2408402 B/op 22002 allocs/op -// BenchmarkUnmarshalBlock/UnmarshalBlock-10000-txs-12 62 19006318 ns/op 24082217 B/op 220002 allocs/op -// BenchmarkUnmarshalBlock/UnmarshalBlock-100000-txs-12 7 186255976 ns/op 240803090 B/op 2200002 allocs/op +// BenchmarkUnmarshalBlock/UnmarshalBlock-0-txs-10 977838 1111 ns/op 176 B/op 1 allocs/op +// BenchmarkUnmarshalBlock/UnmarshalBlock-1-txs-10 658054 1817 ns/op 904 B/op 10 allocs/op +// BenchmarkUnmarshalBlock/UnmarshalBlock-10-txs-10 150804 7924 ns/op 7456 B/op 82 allocs/op +// BenchmarkUnmarshalBlock/UnmarshalBlock-1000-txs-10 1689 684934 ns/op 728377 B/op 8002 allocs/op +// BenchmarkUnmarshalBlock/UnmarshalBlock-10000-txs-10 172 6897857 ns/op 7282186 B/op 80002 allocs/op +// BenchmarkUnmarshalBlock/UnmarshalBlock-100000-txs-10 19 66020735 ns/op 72803047 B/op 800002 allocs/op func BenchmarkUnmarshalBlock(b *testing.B) { for _, numTxs := range []int{0, 1, 10, 1_000, 10_000, 100_000} { b.Run(fmt.Sprintf("UnmarshalBlock-%d-txs", numTxs), func(b *testing.B) { diff --git a/chain/transaction_marshaller.go b/chain/transaction_marshaller.go index 8bf9930aef..353edf8bc8 100644 --- a/chain/transaction_marshaller.go +++ b/chain/transaction_marshaller.go @@ -7,35 +7,37 @@ import ( "fmt" "github.com/StephenButtolph/canoto" - - "github.com/ava-labs/hypersdk/codec" ) //go:generate go run github.com/StephenButtolph/canoto/canoto $GOFILE var _ Parser = (*TxTypeParser)(nil) +type TypeParser[T any] interface { + Unmarshal([]byte) (T, error) +} + type TxTypeParser struct { - ActionRegistry *codec.TypeParser[Action] - AuthRegistry *codec.TypeParser[Auth] + ActionParser TypeParser[Action] + AuthParser TypeParser[Auth] } func NewTxTypeParser( - actionRegistry *codec.TypeParser[Action], - authRegistry *codec.TypeParser[Auth], + actionParser TypeParser[Action], + authParser TypeParser[Auth], ) *TxTypeParser { return &TxTypeParser{ - ActionRegistry: actionRegistry, - AuthRegistry: authRegistry, + ActionParser: actionParser, + AuthParser: authParser, } } func (t *TxTypeParser) ParseAction(bytes []byte) (Action, error) { - return t.ActionRegistry.Unmarshal(bytes) + return t.ActionParser.Unmarshal(bytes) } func (t *TxTypeParser) ParseAuth(bytes []byte) (Auth, error) { - return t.AuthRegistry.Unmarshal(bytes) + return t.AuthParser.Unmarshal(bytes) } type BatchedTransactions struct { diff --git a/chain/transaction_test.go b/chain/transaction_test.go index 9a95e07eab..57cb6cd7aa 100644 --- a/chain/transaction_test.go +++ b/chain/transaction_test.go @@ -25,7 +25,7 @@ import ( externalfees "github.com/ava-labs/hypersdk/fees" ) -const signedTxHex = "0a3208e0f69493af64122001020304050607000000000000000000000000000000000000000000000000001987d612000000000012360000000000000000010000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffff1a5c00000000000000000101020300000000000000000000000000000000000000000000000000000000000001020300000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffff" +const signedTxHex = "0a3208e0f69493af64122001020304050607000000000000000000000000000000000000000000000000001987d61200000000001207000801480150011a4d00080112210102030000000000000000000000000000000000000000000000000000000000001a2101020300000000000000000000000000000000000000000000000000000000000028013001" var preSignedTxBytes []byte @@ -178,7 +178,7 @@ func TestUnmarshalTx(t *testing.T) { // goos: darwin // goarch: arm64 // pkg: github.com/ava-labs/hypersdk/chain -// BenchmarkUnmarshalTx-12 452396 3008 ns/op 2336 B/op 20 allocs/op +// BenchmarkUnmarshalTx-10 917596 1222 ns/op 720 B/op 8 allocs/op // PASS // ok github.com/ava-labs/hypersdk/chain 1.748s func BenchmarkUnmarshalTx(b *testing.B) { diff --git a/cmd/abigen/main.go b/cmd/abigen/main.go deleted file mode 100644 index f1e5f1eab5..0000000000 --- a/cmd/abigen/main.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package main - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - - "github.com/spf13/cobra" - - "github.com/ava-labs/hypersdk/abi" -) - -var ( - packageName string - rootCmd = &cobra.Command{ - Use: "abigen ", - Short: "Generate Go structs from ABI JSON", - Args: cobra.ExactArgs(2), - RunE: run, - } -) - -func init() { - rootCmd.Flags().StringVarP(&packageName, "package", "p", "", "Package name for generated code (overrides default)") -} - -func run(_ *cobra.Command, args []string) error { - inputFile := args[0] - outputFile := args[1] - - // Read the input ABI JSON file - abiData, err := os.ReadFile(inputFile) - if err != nil { - return fmt.Errorf("error reading input file: %w", err) - } - - // Parse the ABI JSON - var vmABI abi.ABI - err = json.Unmarshal(abiData, &vmABI) - if err != nil { - return fmt.Errorf("error parsing ABI JSON: %w", err) - } - - if packageName == "" { - packageName = filepath.Base(filepath.Dir(outputFile)) - } - - // Generate Go structs - generatedCode, err := abi.GenerateGoStructs(vmABI, packageName) - if err != nil { - return fmt.Errorf("error generating Go structs: %w", err) - } - - // Create the directory for the output file if it doesn't exist - outputDir := filepath.Dir(outputFile) - if err := os.MkdirAll(outputDir, 0o755); err != nil { - return fmt.Errorf("error creating output directory: %w", err) - } - - // Write the generated code to the output file - err = os.WriteFile(outputFile, []byte(generatedCode), 0o600) - if err != nil { - return fmt.Errorf("error writing output file: %w", err) - } - - fmt.Printf("Successfully generated Go structs in %s\n", outputFile) - return nil -} - -func main() { - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } -} diff --git a/cmd/hypersdk-cli/README.md b/cmd/hypersdk-cli/README.md index e5db186581..af614e93a2 100644 --- a/cmd/hypersdk-cli/README.md +++ b/cmd/hypersdk-cli/README.md @@ -103,7 +103,7 @@ hypersdk-cli actions -o json Simulate a single action transaction. ```bash -hypersdk-cli read Transfer --data to=0x000000000000000000000000000000000000000000000000000000000000000000a7396ce9,value=12,memo=0xdeadc0de +hypersdk-cli read Transfer --data To=0x003d0ad12b8ee8928edf248ca91ca55600fb383f07c32bff1d6dec472b25cf59a74e575a5a,Value=12,Memo=0xdeadc0de ``` For interactive input remove --data from the command line: @@ -117,7 +117,7 @@ hypersdk-cli read Transfer Send a transaction with a single action. ```bash -hypersdk-cli tx Transfer --data to=0x000000000000000000000000000000000000000000000000000000000000000000a7396ce9,value=12,memo=0x001234 +hypersdk-cli tx Transfer --data To=0x003d0ad12b8ee8928edf248ca91ca55600fb383f07c32bff1d6dec472b25cf59a74e575a5a,Value=12,Memo=0x001234 ``` For interactive input: @@ -140,9 +140,19 @@ If `--sender` isn't provided, the address associated with the private key in ## Notes -- Only flat actions are supported. Arrays, slices, embedded structs, maps, and struct fields are not supported. +- Only flat actions are supported. Futhermore, the following primitive field + types are supported: + - `int8, int16, int32, int64` + - `uint8, uint16, uint32, uint64` + - `bool` + - `string` + - `[]byte` + - `codec.Address` + - `[x]byte` - The CLI supports ED25519 keys only. - If `--data` is supplied or JSON output is selected, the CLI will not ask for action arguments interactively. +- Currently, passing in zero values (e.g. `codec.EmptyAddress`) to the CLI will + result in errors when unmarshaling actions/outputs. This is a known [issue](https://github.com/StephenButtolph/canoto/issues/117) in Canoto. ## Known Issues diff --git a/cmd/hypersdk-cli/abi.go b/cmd/hypersdk-cli/abi.go new file mode 100644 index 0000000000..96c1f994d9 --- /dev/null +++ b/cmd/hypersdk-cli/abi.go @@ -0,0 +1,317 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "fmt" + "math" + "strconv" + "strings" + + "github.com/StephenButtolph/canoto" + "github.com/spf13/cobra" + + "github.com/ava-labs/hypersdk/cli/prompt" + "github.com/ava-labs/hypersdk/codec" +) + +func fillAction(cmd *cobra.Command, spec *canoto.Spec) (canoto.Any, error) { + // get key-value pairs + inputData, err := cmd.Flags().GetStringToString("data") + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to get data key-value pairs: %w", err) + } + + isJSONOutput, err := isJSONOutputRequested(cmd) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to get output format: %w", err) + } + + isInteractive := len(inputData) == 0 && !isJSONOutput + + var kvPairs canoto.Any + if isInteractive { + kvPairs, err = askForFlags(spec) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to ask for flags: %w", err) + } + } else { + kvPairs, err = fillFromInputData(spec, inputData) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to fill from kvData: %w", err) + } + } + + return kvPairs, nil +} + +func unmarshalOutput(spec *canoto.Spec, b []byte) (canoto.Any, error) { + a, err := canoto.Unmarshal(spec, b) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to unmarshal output: %w", err) + } + return a, nil +} + +func fillFromInputData(spec *canoto.Spec, kvData map[string]string) (canoto.Any, error) { + // Require exact match in required fields to supplied arguments + if len(kvData) != len(spec.Fields) { + return canoto.Any{}, fmt.Errorf("type has %d fields, got %d arguments", len(spec.Fields), len(kvData)) + } + for i := range spec.Fields { + if _, ok := kvData[spec.Fields[i].Name]; !ok { + return canoto.Any{}, fmt.Errorf("missing argument: %s", spec.Fields[i].Name) + } + } + + a := canoto.Any{Fields: make([]canoto.AnyField, 0)} + for i := range spec.Fields { + value := kvData[spec.Fields[i].Name] + switch spec.Fields[i].CachedWhichOneOfType() { + case 6: // Int + v, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to parse int: %w", err) + } + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: v, + }) + case 7: // Uint + v, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to parse uint: %w", err) + } + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: v, + }) + case 10: // Bool + v, err := strconv.ParseBool(value) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to parse bool: %w", err) + } + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: v, + }) + case 11: // String + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: value, + }) + case 12: // Bytes + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: []byte(value), + }) + case 13: // Fixed Bytes + // Special case: address + if spec.Fields[i].TypeFixedBytes == codec.AddressLen { + v, err := codec.StringToAddress(value) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to parse address: %w", err) + } + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: v[:], + }) + } else { + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: []byte(value)[:], //nolint: gocritic + }) + } + default: + return canoto.Any{}, fmt.Errorf("unsupported field type: %s", spec.Fields[i].Name) + } + } + + return a, nil +} + +func askForFlags(spec *canoto.Spec) (canoto.Any, error) { + a := canoto.Any{Fields: make([]canoto.AnyField, 0)} + for i := range spec.Fields { + switch spec.Fields[i].CachedWhichOneOfType() { + case 6: // Int + switch spec.Fields[i].TypeInt { + case 1: // Int8 + v, err := prompt.Int(spec.Fields[i].Name, math.MaxInt8) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to get int8: %w", err) + } + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: int64(v), + }) + case 2: // Int16 + v, err := prompt.Int(spec.Fields[i].Name, math.MaxInt16) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to get int16: %w", err) + } + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: int64(v), + }) + case 3: // Int32 + v, err := prompt.Int(spec.Fields[i].Name, math.MaxInt32) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to get int32: %w", err) + } + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: int64(v), + }) + case 4: // Int64 + v, err := prompt.Int(spec.Fields[i].Name, math.MaxInt64) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to get int64: %w", err) + } + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: int64(v), + }) + } + case 7: // Uint + switch spec.Fields[i].TypeUint { + case 1: // Uint8 + v, err := prompt.Uint(spec.Fields[i].Name, math.MaxUint8) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to get uint8: %w", err) + } + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: uint64(v), + }) + case 2: // Uint16 + v, err := prompt.Uint(spec.Fields[i].Name, math.MaxUint16) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to get uint16: %w", err) + } + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: uint64(v), + }) + case 3: // Uint32 + v, err := prompt.Uint(spec.Fields[i].Name, math.MaxUint32) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to get uint32: %w", err) + } + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: uint64(v), + }) + case 4: // Uint64 + v, err := prompt.Uint(spec.Fields[i].Name, math.MaxUint64) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to get uint64: %w", err) + } + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: uint64(v), + }) + } + case 10: // Bool + v, err := prompt.Bool(spec.Fields[i].Name) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to get bool: %w", err) + } + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: v, + }) + case 11: // String + // TODO: fine tune max length + v, err := prompt.String(spec.Fields[i].Name, 0, 1024) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to get string: %w", err) + } + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: v, + }) + case 12: // Bytes + v, err := prompt.Bytes(spec.Fields[i].Name) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to get bytes: %w", err) + } + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: v, + }) + case 13: // Fixed Bytes + // Special case: address + if spec.Fields[i].TypeFixedBytes == codec.AddressLen { + v, err := prompt.Address(spec.Fields[i].Name) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to get address: %w", err) + } + if len(v) != codec.AddressLen { + return canoto.Any{}, fmt.Errorf("invalid address length: %d", len(v)) + } + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: v[:], + }) + } else { + v, err := prompt.Bytes(spec.Fields[i].Name) + if err != nil { + return canoto.Any{}, fmt.Errorf("failed to get fixed bytes: %w", err) + } + a.Fields = append(a.Fields, canoto.AnyField{ + Name: spec.Fields[i].Name, + Value: v[:], //nolint: gocritic + }) + } + default: + return canoto.Any{}, fmt.Errorf("unsupported field type: %s", spec.Fields[i].Name) + } + } + return a, nil +} + +func getType(fieldType *canoto.FieldType) string { + switch fieldType.CachedWhichOneOfType() { + case 6: // Int + switch fieldType.TypeInt { + case 1: // Int8 + return "int8" + case 2: // Int16 + return "int16" + case 3: // Int32 + return "int32" + case 4: // Int64 + return "int64" + } + case 7: // Uint + switch fieldType.TypeUint { + case 1: // Uint8 + return "uint8" + case 2: // Uint16 + return "uint16" + case 3: // Uint32 + return "uint32" + case 4: // Uint64 + return "uint64" + } + case 10: // Bool + return "bool" + case 11: // String + return "string" + case 12: // Bytes + return "bytes" + case 13: // Fixed Bytes + // Special case: address + if fieldType.TypeFixedBytes == codec.AddressLen { + return "address" + } + var str strings.Builder + str.WriteString("[") + str.WriteString(strconv.FormatUint(fieldType.TypeFixedBytes, 10)) + str.WriteString("]byte") + return str.String() + } + return "unknown type" +} diff --git a/cmd/hypersdk-cli/actions.go b/cmd/hypersdk-cli/actions.go index d82d08fbae..18a9039229 100644 --- a/cmd/hypersdk-cli/actions.go +++ b/cmd/hypersdk-cli/actions.go @@ -33,40 +33,18 @@ var actionsCmd = &cobra.Command{ } type abiWrapper struct { - ABI abi.ABI + ABI *abi.ABI } func (a abiWrapper) String() string { result := "" - for _, action := range a.ABI.Actions { + for _, action := range a.ABI.ActionsSpec { result += fmt.Sprintf("---\n%s\n\n", action.Name) - typ, found := a.ABI.FindTypeByName(action.Name) - if !found { - result += fmt.Sprintf(" Error: Type not found for action %s\n", action.Name) - continue - } else { - result += "Inputs:\n" - for _, field := range typ.Fields { - result += fmt.Sprintf(" %s: %s\n", field.Name, field.Type) - } - } - - output, found := a.ABI.FindOutputByID(action.ID) - if !found { - result += fmt.Sprintf("No outputs for %s with id %d\n", action.Name, action.ID) - continue - } - - typ, found = a.ABI.FindTypeByName(output.Name) - if !found { - result += fmt.Sprintf(" Error: Type not found for output %s\n", output.Name) - continue - } - result += "\nOutputs:\n" - for _, field := range typ.Fields { - result += fmt.Sprintf(" %s: %s\n", field.Name, field.Type) + for i := range action.Fields { + result += fmt.Sprintf(" %s: %s\n", action.Fields[i].Name, getType(&action.Fields[i])) } } + return result } diff --git a/cmd/hypersdk-cli/read.go b/cmd/hypersdk-cli/read.go index 4ce610dd50..d2598a18b7 100644 --- a/cmd/hypersdk-cli/read.go +++ b/cmd/hypersdk-cli/read.go @@ -8,17 +8,13 @@ import ( "encoding/json" "errors" "fmt" - "math" - "strconv" "strings" + "github.com/StephenButtolph/canoto" "github.com/spf13/cobra" - "github.com/ava-labs/hypersdk/abi" - "github.com/ava-labs/hypersdk/abi/dynamic" "github.com/ava-labs/hypersdk/api/jsonrpc" "github.com/ava-labs/hypersdk/auth" - "github.com/ava-labs/hypersdk/cli/prompt" "github.com/ava-labs/hypersdk/codec" ) @@ -70,196 +66,70 @@ var readCmd = &cobra.Command{ return errors.New("action name is required") } actionName := args[0] - _, found := abi.FindActionByName(actionName) - if !found { - return fmt.Errorf("failed to find action: %s", actionName) - } - - typ, found := abi.FindTypeByName(actionName) - if !found { - return fmt.Errorf("failed to find type: %s", actionName) + spec, ok := abi.FindActionSpecByName(actionName) + if !ok { + return fmt.Errorf("failed to find action spec: %s", actionName) } // 5. create action using kvPairs - kvPairs, err := fillAction(cmd, typ) - if err != nil { - return err - } - - jsonPayload, err := json.Marshal(kvPairs) + action, err := fillAction(cmd, spec) if err != nil { - return fmt.Errorf("failed to marshal kvPairs: %w", err) + return fmt.Errorf("failed to fill action: %w", err) } - actionBytes, err := dynamic.Marshal(abi, actionName, string(jsonPayload)) + actionBytes, err := canoto.Marshal(spec, action) if err != nil { return fmt.Errorf("failed to marshal action: %w", err) } - results, executeErr := client.ExecuteActions(context.Background(), sender, [][]byte{actionBytes}) - var resultStruct map[string]interface{} - - if len(results) == 1 { - resultJSON, err := dynamic.UnmarshalOutput(abi, results[0]) - if err != nil { - return fmt.Errorf("failed to unmarshal result: %w", err) - } - - err = json.Unmarshal([]byte(resultJSON), &resultStruct) - if err != nil { - return fmt.Errorf("failed to unmarshal result JSON: %w", err) - } - } - - errorString := "" - if executeErr != nil { - errorString = executeErr.Error() + typeID, ok := abi.GetActionID(actionName) + if !ok { + return fmt.Errorf("failed to get action ID: %s", actionName) } - return printValue(cmd, readResponse{ - Result: resultStruct, - Success: executeErr == nil, - Error: errorString, - }) - }, -} - -func fillAction(cmd *cobra.Command, typ abi.Type) (map[string]interface{}, error) { - // get key-value pairs - inputData, err := cmd.Flags().GetStringToString("data") - if err != nil { - return nil, fmt.Errorf("failed to get data key-value pairs: %w", err) - } - - isJSONOutput, err := isJSONOutputRequested(cmd) - if err != nil { - return nil, fmt.Errorf("failed to get output format: %w", err) - } - - isInteractive := len(inputData) == 0 && !isJSONOutput + actionBytes = append([]byte{typeID}, actionBytes...) - var kvPairs map[string]interface{} - if isInteractive { - kvPairs, err = askForFlags(typ) + results, err := client.ExecuteActions(context.Background(), sender, [][]byte{actionBytes}) if err != nil { - return nil, fmt.Errorf("failed to ask for flags: %w", err) + return fmt.Errorf("failed to execute action: %w", err) } - } else { - kvPairs, err = fillFromInputData(typ, inputData) - if err != nil { - return nil, fmt.Errorf("failed to fill from kvData: %w", err) - } - } - return kvPairs, nil -} - -func fillFromInputData(typ abi.Type, kvData map[string]string) (map[string]interface{}, error) { - // Require exact match in required fields to supplied arguments - if len(kvData) != len(typ.Fields) { - return nil, fmt.Errorf("type has %d fields, got %d arguments", len(typ.Fields), len(kvData)) - } - for _, field := range typ.Fields { - if _, ok := kvData[field.Name]; !ok { - return nil, fmt.Errorf("missing argument: %s", field.Name) + if len(results) == 0 { + return errors.New("no results returned") } - } - kvPairs := make(map[string]interface{}) - for _, field := range typ.Fields { - value := kvData[field.Name] - var parsedValue interface{} - var err error - switch field.Type { - case "Address": - parsedValue = value - case "uint8", "uint16", "uint32", "uint", "uint64": - parsedValue, err = strconv.ParseUint(value, 10, 64) - case "int8", "int16", "int32", "int", "int64": - parsedValue, err = strconv.ParseInt(value, 10, 64) - case "[]uint8": - if value == "" { - parsedValue = []uint8{} - } else { - parsedValue, err = codec.LoadHex(value, -1) - } - case "string": - parsedValue = value - case "bool": - parsedValue, err = strconv.ParseBool(value) - default: - return nil, fmt.Errorf("unsupported field type: %s", field.Type) + outputBytes := results[0] + if len(outputBytes) == 0 { + return errors.New("empty output bytes") } - if err != nil { - return nil, fmt.Errorf("failed to parse %s: %w", field.Name, err) - } - kvPairs[field.Name] = parsedValue - } - return kvPairs, nil -} - -func askForFlags(typ abi.Type) (map[string]interface{}, error) { - kvPairs := make(map[string]interface{}) - for _, field := range typ.Fields { - var err error - var value interface{} - switch field.Type { - case "Address": - value, err = prompt.Address(field.Name) - case "uint8": - value, err = prompt.Uint(field.Name, math.MaxUint8) - case "uint16": - value, err = prompt.Uint(field.Name, math.MaxUint16) - case "uint32": - value, err = prompt.Uint(field.Name, math.MaxUint32) - case "uint", "uint64": - value, err = prompt.Uint(field.Name, math.MaxUint64) - case "int8": - value, err = prompt.Int(field.Name, math.MaxInt8) - case "int16": - value, err = prompt.Int(field.Name, math.MaxInt16) - case "int32": - value, err = prompt.Int(field.Name, math.MaxInt32) - case "int", "int64": - value, err = prompt.Int(field.Name, math.MaxInt64) - case "[]uint8": - value, err = prompt.Bytes(field.Name) - case "string": - value, err = prompt.String(field.Name, 0, 1024) - case "bool": - value, err = prompt.Bool(field.Name) - default: - return nil, fmt.Errorf("unsupported field type in CLI: %s", field.Type) + outputTypeID := outputBytes[0] + outputSpec, ok := abi.FindOutputSpecByID(outputTypeID) + if !ok { + return fmt.Errorf("failed to find output spec: %d", outputTypeID) } + + output, err := unmarshalOutput(outputSpec, outputBytes[1:]) if err != nil { - return nil, fmt.Errorf("failed to get input for %s field: %w", field.Name, err) + return fmt.Errorf("failed to unmarshal output: %w", err) } - kvPairs[field.Name] = value - } - return kvPairs, nil + + return printValue(cmd, readResponse{Result: output}) + }, } type readResponse struct { - Result map[string]interface{} `json:"result"` - Success bool `json:"success"` - Error string `json:"error"` + Result canoto.Any `json:"result"` } func (r readResponse) String() string { var result strings.Builder - if r.Success { - result.WriteString("✅ Read-only execution successful:\n") - for key, value := range r.Result { - jsonValue, err := json.Marshal(value) - if err != nil { - jsonValue = []byte(fmt.Sprintf("%v", value)) - } - result.WriteString(fmt.Sprintf("%s: %s\n", key, string(jsonValue))) - } - } else { - result.WriteString(fmt.Sprintf("❌ Read-only execution failed: %s\n", r.Error)) + result.WriteString("✅ Read-only execution successful:\n") + jsonValue, err := json.Marshal(r.Result) + if err != nil { + jsonValue = []byte(fmt.Sprintf("%v", r.Result)) } + result.WriteString(fmt.Sprintf("Result: %s\n", string(jsonValue))) return result.String() } diff --git a/cmd/hypersdk-cli/transaction.go b/cmd/hypersdk-cli/transaction.go index 976b0fde59..7824c625b6 100644 --- a/cmd/hypersdk-cli/transaction.go +++ b/cmd/hypersdk-cli/transaction.go @@ -11,10 +11,10 @@ import ( "strings" "time" + "github.com/StephenButtolph/canoto" "github.com/ava-labs/avalanchego/ids" "github.com/spf13/cobra" - "github.com/ava-labs/hypersdk/abi/dynamic" "github.com/ava-labs/hypersdk/api/indexer" "github.com/ava-labs/hypersdk/api/jsonrpc" "github.com/ava-labs/hypersdk/auth" @@ -50,37 +50,35 @@ var txCmd = &cobra.Command{ if err != nil { return fmt.Errorf("failed to get abi: %w", err) } + // 4. get action name from args if len(args) == 0 { return errors.New("action name is required") } actionName := args[0] - _, found := abi.FindActionByName(actionName) - if !found { - return fmt.Errorf("failed to find action: %s", actionName) + spec, ok := abi.FindActionSpecByName(actionName) + if !ok { + return fmt.Errorf("failed to find action spec: %s", actionName) } - typ, found := abi.FindTypeByName(actionName) - if !found { - return fmt.Errorf("failed to find type: %s", actionName) + typeID, ok := abi.GetActionID(actionName) + if !ok { + return fmt.Errorf("failed to get action ID: %s", actionName) } // 5. create action using kvPairs - kvPairs, err := fillAction(cmd, typ) - if err != nil { - return err - } - - jsonPayload, err := json.Marshal(kvPairs) + action, err := fillAction(cmd, spec) if err != nil { - return fmt.Errorf("failed to marshal kvPairs: %w", err) + return fmt.Errorf("failed to fill action: %w", err) } - actionBytes, err := dynamic.Marshal(abi, actionName, string(jsonPayload)) + actionBytes, err := canoto.Marshal(spec, action) if err != nil { return fmt.Errorf("failed to marshal action: %w", err) } + actionBytes = append([]byte{typeID}, actionBytes...) + _, _, chainID, err := client.Network(ctx) if err != nil { return fmt.Errorf("failed to get network info: %w", err) @@ -109,35 +107,40 @@ var txCmd = &cobra.Command{ return fmt.Errorf("context expired while waiting for tx: %w", err) } - getTxResponse, found, err = indexerClient.GetTxResults(ctx, expectedTxID) + resp, ok, err := indexerClient.GetTxResults(ctx, expectedTxID) if err != nil { return fmt.Errorf("failed to get tx: %w", err) } - if found { + if ok { + getTxResponse = resp break } time.Sleep(500 * time.Millisecond) } - var resultStruct map[string]interface{} - if getTxResponse.Result.Success { - if len(getTxResponse.Result.Outputs) == 1 { - resultJSON, err := dynamic.UnmarshalOutput(abi, getTxResponse.Result.Outputs[0]) - if err != nil { - return fmt.Errorf("failed to unmarshal result: %w", err) - } - - err = json.Unmarshal([]byte(resultJSON), &resultStruct) - if err != nil { - return fmt.Errorf("failed to unmarshal result JSON: %w", err) - } - } else if len(getTxResponse.Result.Outputs) > 1 { - return fmt.Errorf("expected 1 output, got %d", len(getTxResponse.Result.Outputs)) - } + if len(getTxResponse.Result.Outputs) == 0 { + return fmt.Errorf("no outputs found for tx: %s", expectedTxID) + } + + outputBytes := getTxResponse.Result.Outputs[0] + if len(outputBytes) == 0 { + return fmt.Errorf("empty output for tx: %s", expectedTxID) + } + + // Get type ID + outputTypeID := outputBytes[0] + outputSpec, ok := abi.FindOutputSpecByID(outputTypeID) + if !ok { + return fmt.Errorf("failed to find output spec: %d", outputTypeID) + } + + output, err := unmarshalOutput(outputSpec, outputBytes[1:]) + if err != nil { + return fmt.Errorf("failed to unmarshal output: %w", err) } return printValue(cmd, txResponse{ - Result: resultStruct, + Result: output, Success: getTxResponse.Result.Success, TxID: expectedTxID, Error: string(getTxResponse.Result.Error), @@ -146,24 +149,22 @@ var txCmd = &cobra.Command{ } type txResponse struct { - Result map[string]interface{} `json:"result"` - Success bool `json:"success"` - TxID ids.ID `json:"txId"` - Error string `json:"error"` + Result canoto.Any `json:"result"` + Success bool `json:"success"` + TxID ids.ID `json:"txId"` + Error string `json:"error"` } func (r txResponse) String() string { var result strings.Builder if r.Success { result.WriteString(fmt.Sprintf("✅ Transaction successful (txID: %s)\n", r.TxID)) - if r.Result != nil { - for key, value := range r.Result { - jsonValue, err := json.Marshal(value) - if err != nil { - jsonValue = []byte(fmt.Sprintf("%v", value)) - } - result.WriteString(fmt.Sprintf("%s: %s\n", key, string(jsonValue))) + if len(r.Result.Fields) != 0 { + jsonValue, err := json.Marshal(r.Result) + if err != nil { + jsonValue = []byte(fmt.Sprintf("%v", r.Result)) } + result.WriteString(fmt.Sprintf("Result: %s\n", string(jsonValue))) } } else { result.WriteString(fmt.Sprintf("❌ Transaction failed (txID: %s): %s\n", r.TxID, r.Error)) diff --git a/codec/canoto_parser.canoto.go b/codec/canoto_parser.canoto.go new file mode 100644 index 0000000000..963edb7e71 --- /dev/null +++ b/codec/canoto_parser.canoto.go @@ -0,0 +1,203 @@ +// Code generated by canoto. DO NOT EDIT. +// versions: +// canoto v0.15.0 +// source: canoto_parser.go + +package codec + +import ( + "io" + "reflect" + "sync/atomic" + + "github.com/StephenButtolph/canoto" +) + +// Ensure that unused imports do not error +var ( + _ atomic.Uint64 + + _ = io.ErrUnexpectedEOF +) + +const ( + canoto__TypedStruct__Name__tag = "\x0a" // canoto.Tag(1, canoto.Len) + canoto__TypedStruct__ID__tag = "\x10" // canoto.Tag(2, canoto.Varint) +) + +type canotoData_TypedStruct struct { + size atomic.Uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*TypedStruct) CanotoSpec(...reflect.Type) *canoto.Spec { + var zero TypedStruct + s := &canoto.Spec{ + Name: "TypedStruct", + Fields: []canoto.FieldType{ + { + FieldNumber: 1, + Name: "Name", + OneOf: "", + TypeString: true, + }, + { + FieldNumber: 2, + Name: "ID", + OneOf: "", + TypeUint: canoto.SizeOf(zero.ID), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*TypedStruct) MakeCanoto() *TypedStruct { + return new(TypedStruct) +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *TypedStruct) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a canoto.Reader. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *TypedStruct) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = TypedStruct{} + c.canotoData.size.Store(uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case 1: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadString(&r, &c.Name); err != nil { + return err + } + if len(c.Name) == 0 { + return canoto.ErrZeroValue + } + case 2: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.ID); err != nil { + return err + } + if canoto.IsZero(c.ID) { + return canoto.ErrZeroValue + } + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *TypedStruct) ValidCanoto() bool { + if c == nil { + return true + } + if !canoto.ValidString(c.Name) { + return false + } + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +func (c *TypedStruct) CalculateCanotoCache() { + if c == nil { + return + } + var size uint64 + if len(c.Name) != 0 { + size += uint64(len(canoto__TypedStruct__Name__tag)) + canoto.SizeBytes(c.Name) + } + if !canoto.IsZero(c.ID) { + size += uint64(len(canoto__TypedStruct__ID__tag)) + canoto.SizeUint(c.ID) + } + c.canotoData.size.Store(size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *TypedStruct) CachedCanotoSize() uint64 { + if c == nil { + return 0 + } + return c.canotoData.size.Load() +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *TypedStruct) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a canoto.Writer and returns the +// resulting canoto.Writer. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *TypedStruct) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if c == nil { + return w + } + if len(c.Name) != 0 { + canoto.Append(&w, canoto__TypedStruct__Name__tag) + canoto.AppendBytes(&w, c.Name) + } + if !canoto.IsZero(c.ID) { + canoto.Append(&w, canoto__TypedStruct__ID__tag) + canoto.AppendUint(&w, c.ID) + } + return w +} diff --git a/codec/type_parser.go b/codec/canoto_parser.go similarity index 60% rename from codec/type_parser.go rename to codec/canoto_parser.go index d438d6eb9d..c3983a33ca 100644 --- a/codec/type_parser.go +++ b/codec/canoto_parser.go @@ -3,8 +3,13 @@ package codec +//go:generate go run github.com/StephenButtolph/canoto/canoto $GOFILE + import ( "fmt" + "reflect" + + "github.com/StephenButtolph/canoto" "github.com/ava-labs/hypersdk/consts" ) @@ -13,30 +18,40 @@ type decoder[T Typed] struct { f func([]byte) (T, error) } -// The number of types is limited to 255. -type TypeParser[T Typed] struct { - typeToIndex map[string]uint8 +type Typed interface { + GetTypeID() uint8 +} + +type CanotoTyped interface { + Typed + CanotoSpec(...reflect.Type) *canoto.Spec +} + +type TypedStruct struct { + Name string `canoto:"string,1" json:"name"` + ID uint8 `canoto:"uint,2" json:"id"` + + canotoData canotoData_TypedStruct +} + +type CanotoParser[T Typed] struct { + registeredTypes []*canoto.Spec indexToDecoder map[uint8]*decoder[T] - registeredTypes []Typed + typeToIndex map[string]uint8 } -// NewTypeParser returns an instance of a Typeparser with generic type [T]. -func NewTypeParser[T Typed]() *TypeParser[T] { - return &TypeParser[T]{ - typeToIndex: map[string]uint8{}, +func NewCanotoParser[T Typed]() *CanotoParser[T] { + return &CanotoParser[T]{ + registeredTypes: []*canoto.Spec{}, indexToDecoder: map[uint8]*decoder[T]{}, - registeredTypes: []Typed{}, + typeToIndex: map[string]uint8{}, } } -type Typed interface { - GetTypeID() uint8 -} - // Register registers a new type into TypeParser [p]. Registers the type by using // the string representation of [o], and sets the decoder of that index to [f]. // Returns an error if [o] has already been registered or the TypeParser is full. -func (p *TypeParser[T]) Register(instance Typed, f func([]byte) (T, error)) error { +func (p *CanotoParser[T]) Register(instance CanotoTyped, f func([]byte) (T, error)) error { if len(p.indexToDecoder) == int(consts.MaxUint8)+1 { return ErrTooManyItems } @@ -45,24 +60,17 @@ func (p *TypeParser[T]) Register(instance Typed, f func([]byte) (T, error)) erro return ErrDuplicateItem } p.indexToDecoder[instance.GetTypeID()] = &decoder[T]{f: f} - p.registeredTypes = append(p.registeredTypes, instance) - return nil -} + spec := instance.CanotoSpec() + p.registeredTypes = append(p.registeredTypes, spec) + p.typeToIndex[spec.Name] = instance.GetTypeID() -// lookupIndex returns the decoder function and success of lookup of [index] -// from Typeparser [p]. -func (p *TypeParser[T]) lookupIndex(index uint8) (func([]byte) (T, error), bool) { - d, ok := p.indexToDecoder[index] - if ok { - return d.f, true - } - return nil, false + return nil } // Unmarshal unmarshals a value of type [T] from the reader by unpacking // the typeID and invoking the corresponding decoder function. -func (p *TypeParser[T]) Unmarshal(bytes []byte) (T, error) { +func (p *CanotoParser[T]) Unmarshal(bytes []byte) (T, error) { if len(bytes) == 0 { return *new(T), fmt.Errorf("typeID not found in slice with length %d", len(bytes)) } @@ -81,8 +89,27 @@ func (p *TypeParser[T]) Unmarshal(bytes []byte) (T, error) { return decoder(bytes) } -// GetRegisteredTypes returns all registered types in the TypeParser. -// This is used for generating ABI. -func (p *TypeParser[T]) GetRegisteredTypes() []Typed { +func (p *CanotoParser[T]) GetRegisteredTypes() []*canoto.Spec { return p.registeredTypes } + +func (p *CanotoParser[T]) GetTypedStructs() []TypedStruct { + typedStructs := make([]TypedStruct, 0) + for k, v := range p.typeToIndex { + typedStructs = append(typedStructs, TypedStruct{ + Name: k, + ID: v, + }) + } + return typedStructs +} + +// lookupIndex returns the decoder function and success of lookup of [index] +// from Typeparser [p]. +func (p *CanotoParser[T]) lookupIndex(index uint8) (func([]byte) (T, error), bool) { + d, ok := p.indexToDecoder[index] + if ok { + return d.f, true + } + return nil, false +} diff --git a/codec/type_parser_test.go b/codec/canoto_parser_test.go similarity index 60% rename from codec/type_parser_test.go rename to codec/canoto_parser_test.go index 0cd9b06763..5b53476894 100644 --- a/codec/type_parser_test.go +++ b/codec/canoto_parser_test.go @@ -5,8 +5,10 @@ package codec import ( "errors" + "reflect" "testing" + "github.com/StephenButtolph/canoto" "github.com/stretchr/testify/require" "github.com/ava-labs/hypersdk/consts" @@ -14,6 +16,7 @@ import ( type Blah interface { Typed + CanotoSpec(...reflect.Type) *canoto.Spec Bark() string } @@ -23,29 +26,58 @@ func (*Blah1) Bark() string { return "blah1" } func (*Blah1) GetTypeID() uint8 { return 0 } +func (*Blah1) CanotoSpec(...reflect.Type) *canoto.Spec { + return &canoto.Spec{ + Name: "blah1", + Fields: []canoto.FieldType{}, + } +} + type Blah2 struct{} func (*Blah2) Bark() string { return "blah2" } func (*Blah2) GetTypeID() uint8 { return 1 } +func (*Blah2) CanotoSpec(...reflect.Type) *canoto.Spec { + return &canoto.Spec{ + Name: "blah2", + Fields: []canoto.FieldType{}, + } +} + type Blah3 struct{} func (*Blah3) Bark() string { return "blah3" } func (*Blah3) GetTypeID() uint8 { return 2 } +func (*Blah3) CanotoSpec(...reflect.Type) *canoto.Spec { + return &canoto.Spec{ + Name: "blah3", + Fields: []canoto.FieldType{}, + } +} + type withID struct { ID uint8 } func (w *withID) GetTypeID() uint8 { return w.ID } + +func (*withID) CanotoSpec(...reflect.Type) *canoto.Spec { + return &canoto.Spec{ + Name: "blah3", + Fields: []canoto.FieldType{}, + } +} + func TestTypeParser(t *testing.T) { - tp := NewTypeParser[Blah]() + cp := NewCanotoParser[Blah]() t.Run("empty parser", func(t *testing.T) { require := require.New(t) - f, ok := tp.lookupIndex(0) + f, ok := cp.lookupIndex(0) require.Nil(f) require.False(ok) }) @@ -58,19 +90,19 @@ func TestTypeParser(t *testing.T) { errBlah1 := errors.New("blah1") errBlah2 := errors.New("blah2") require.NoError( - tp.Register(blah1, func([]byte) (Blah, error) { return nil, errBlah1 }), + cp.Register(blah1, func([]byte) (Blah, error) { return nil, errBlah1 }), ) require.NoError( - tp.Register(blah2, func([]byte) (Blah, error) { return nil, errBlah2 }), + cp.Register(blah2, func([]byte) (Blah, error) { return nil, errBlah2 }), ) - f, ok := tp.lookupIndex(blah1.GetTypeID()) + f, ok := cp.lookupIndex(blah1.GetTypeID()) require.True(ok) res, err := f(nil) require.Nil(res) require.ErrorIs(err, errBlah1) - f, ok = tp.lookupIndex(blah2.GetTypeID()) + f, ok = cp.lookupIndex(blah2.GetTypeID()) require.True(ok) res, err = f(nil) require.Nil(res) @@ -79,19 +111,19 @@ func TestTypeParser(t *testing.T) { t.Run("duplicate item", func(t *testing.T) { require := require.New(t) - err := tp.Register(&Blah1{}, nil) + err := cp.Register(&Blah1{}, nil) require.ErrorIs(err, ErrDuplicateItem) }) t.Run("too many items", func(t *testing.T) { require := require.New(t) - arrayLength := int(consts.MaxUint8) + 1 - len(tp.indexToDecoder) + arrayLength := int(consts.MaxUint8) + 1 - len(cp.indexToDecoder) for index := range make([]struct{}, arrayLength) { // 0 and 1 are already existing -> we use index + 2 - require.NoError(tp.Register(&withID{ID: uint8(index + 2)}, nil)) + require.NoError(cp.Register(&withID{ID: uint8(index + 2)}, nil)) } // all possible uint8 value should already be stored, using any return ErrTooManyItems - err := tp.Register(&withID{ID: uint8(4)}, nil) + err := cp.Register(&withID{ID: uint8(4)}, nil) require.ErrorIs(err, ErrTooManyItems) }) } diff --git a/examples/morpheusvm/actions/transfer.canoto.go b/examples/morpheusvm/actions/transfer.canoto.go new file mode 100644 index 0000000000..f59d797e01 --- /dev/null +++ b/examples/morpheusvm/actions/transfer.canoto.go @@ -0,0 +1,418 @@ +// Code generated by canoto. DO NOT EDIT. +// versions: +// canoto v0.15.0 +// source: transfer.go + +package actions + +import ( + "io" + "reflect" + "sync/atomic" + + "github.com/StephenButtolph/canoto" +) + +// Ensure that unused imports do not error +var ( + _ atomic.Uint64 + + _ = io.ErrUnexpectedEOF +) + +const ( + canoto__Transfer__To__tag = "\x0a" // canoto.Tag(1, canoto.Len) + canoto__Transfer__Value__tag = "\x10" // canoto.Tag(2, canoto.Varint) + canoto__Transfer__Memo__tag = "\x1a" // canoto.Tag(3, canoto.Len) +) + +type canotoData_Transfer struct { + size atomic.Uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*Transfer) CanotoSpec(...reflect.Type) *canoto.Spec { + var zero Transfer + s := &canoto.Spec{ + Name: "Transfer", + Fields: []canoto.FieldType{ + { + FieldNumber: 1, + Name: "To", + OneOf: "", + TypeFixedBytes: uint64(len(zero.To)), + }, + { + FieldNumber: 2, + Name: "Value", + OneOf: "", + TypeUint: canoto.SizeOf(zero.Value), + }, + { + FieldNumber: 3, + Name: "Memo", + OneOf: "", + TypeBytes: true, + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*Transfer) MakeCanoto() *Transfer { + return new(Transfer) +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *Transfer) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a canoto.Reader. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *Transfer) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = Transfer{} + c.canotoData.size.Store(uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case 1: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + const ( + expectedLength = len(c.To) + expectedLengthUint64 = uint64(expectedLength) + ) + var length uint64 + if err := canoto.ReadUint(&r, &length); err != nil { + return err + } + if length != expectedLengthUint64 { + return canoto.ErrInvalidLength + } + if expectedLength > len(r.B) { + return io.ErrUnexpectedEOF + } + + copy((&c.To)[:], r.B) + if canoto.IsZero(c.To) { + return canoto.ErrZeroValue + } + r.B = r.B[expectedLength:] + case 2: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.Value); err != nil { + return err + } + if canoto.IsZero(c.Value) { + return canoto.ErrZeroValue + } + case 3: + if wireType != canoto.Len { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadBytes(&r, &c.Memo); err != nil { + return err + } + if len(c.Memo) == 0 { + return canoto.ErrZeroValue + } + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *Transfer) ValidCanoto() bool { + if c == nil { + return true + } + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +func (c *Transfer) CalculateCanotoCache() { + if c == nil { + return + } + var size uint64 + if !canoto.IsZero(c.To) { + size += uint64(len(canoto__Transfer__To__tag)) + canoto.SizeBytes((&c.To)[:]) + } + if !canoto.IsZero(c.Value) { + size += uint64(len(canoto__Transfer__Value__tag)) + canoto.SizeUint(c.Value) + } + if len(c.Memo) != 0 { + size += uint64(len(canoto__Transfer__Memo__tag)) + canoto.SizeBytes(c.Memo) + } + c.canotoData.size.Store(size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *Transfer) CachedCanotoSize() uint64 { + if c == nil { + return 0 + } + return c.canotoData.size.Load() +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *Transfer) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a canoto.Writer and returns the +// resulting canoto.Writer. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *Transfer) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if c == nil { + return w + } + if !canoto.IsZero(c.To) { + canoto.Append(&w, canoto__Transfer__To__tag) + canoto.AppendBytes(&w, (&c.To)[:]) + } + if !canoto.IsZero(c.Value) { + canoto.Append(&w, canoto__Transfer__Value__tag) + canoto.AppendUint(&w, c.Value) + } + if len(c.Memo) != 0 { + canoto.Append(&w, canoto__Transfer__Memo__tag) + canoto.AppendBytes(&w, c.Memo) + } + return w +} + +const ( + canoto__TransferResult__SenderBalance__tag = "\x08" // canoto.Tag(1, canoto.Varint) + canoto__TransferResult__ReceiverBalance__tag = "\x10" // canoto.Tag(2, canoto.Varint) +) + +type canotoData_TransferResult struct { + size atomic.Uint64 +} + +// CanotoSpec returns the specification of this canoto message. +func (*TransferResult) CanotoSpec(...reflect.Type) *canoto.Spec { + var zero TransferResult + s := &canoto.Spec{ + Name: "TransferResult", + Fields: []canoto.FieldType{ + { + FieldNumber: 1, + Name: "SenderBalance", + OneOf: "", + TypeUint: canoto.SizeOf(zero.SenderBalance), + }, + { + FieldNumber: 2, + Name: "ReceiverBalance", + OneOf: "", + TypeUint: canoto.SizeOf(zero.ReceiverBalance), + }, + }, + } + s.CalculateCanotoCache() + return s +} + +// MakeCanoto creates a new empty value. +func (*TransferResult) MakeCanoto() *TransferResult { + return new(TransferResult) +} + +// UnmarshalCanoto unmarshals a Canoto-encoded byte slice into the struct. +// +// During parsing, the canoto cache is saved. +func (c *TransferResult) UnmarshalCanoto(bytes []byte) error { + r := canoto.Reader{ + B: bytes, + } + return c.UnmarshalCanotoFrom(r) +} + +// UnmarshalCanotoFrom populates the struct from a canoto.Reader. Most users +// should just use UnmarshalCanoto. +// +// During parsing, the canoto cache is saved. +// +// This function enables configuration of reader options. +func (c *TransferResult) UnmarshalCanotoFrom(r canoto.Reader) error { + // Zero the struct before unmarshaling. + *c = TransferResult{} + c.canotoData.size.Store(uint64(len(r.B))) + + var minField uint32 + for canoto.HasNext(&r) { + field, wireType, err := canoto.ReadTag(&r) + if err != nil { + return err + } + if field < minField { + return canoto.ErrInvalidFieldOrder + } + + switch field { + case 1: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.SenderBalance); err != nil { + return err + } + if canoto.IsZero(c.SenderBalance) { + return canoto.ErrZeroValue + } + case 2: + if wireType != canoto.Varint { + return canoto.ErrUnexpectedWireType + } + + if err := canoto.ReadUint(&r, &c.ReceiverBalance); err != nil { + return err + } + if canoto.IsZero(c.ReceiverBalance) { + return canoto.ErrZeroValue + } + default: + return canoto.ErrUnknownField + } + + minField = field + 1 + } + return nil +} + +// ValidCanoto validates that the struct can be correctly marshaled into the +// Canoto format. +// +// Specifically, ValidCanoto ensures: +// 1. All OneOfs are specified at most once. +// 2. All strings are valid utf-8. +// 3. All custom fields are ValidCanoto. +func (c *TransferResult) ValidCanoto() bool { + if c == nil { + return true + } + return true +} + +// CalculateCanotoCache populates size and OneOf caches based on the current +// values in the struct. +func (c *TransferResult) CalculateCanotoCache() { + if c == nil { + return + } + var size uint64 + if !canoto.IsZero(c.SenderBalance) { + size += uint64(len(canoto__TransferResult__SenderBalance__tag)) + canoto.SizeUint(c.SenderBalance) + } + if !canoto.IsZero(c.ReceiverBalance) { + size += uint64(len(canoto__TransferResult__ReceiverBalance__tag)) + canoto.SizeUint(c.ReceiverBalance) + } + c.canotoData.size.Store(size) +} + +// CachedCanotoSize returns the previously calculated size of the Canoto +// representation from CalculateCanotoCache. +// +// If CalculateCanotoCache has not yet been called, it will return 0. +// +// If the struct has been modified since the last call to CalculateCanotoCache, +// the returned size may be incorrect. +func (c *TransferResult) CachedCanotoSize() uint64 { + if c == nil { + return 0 + } + return c.canotoData.size.Load() +} + +// MarshalCanoto returns the Canoto representation of this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *TransferResult) MarshalCanoto() []byte { + c.CalculateCanotoCache() + w := canoto.Writer{ + B: make([]byte, 0, c.CachedCanotoSize()), + } + w = c.MarshalCanotoInto(w) + return w.B +} + +// MarshalCanotoInto writes the struct into a canoto.Writer and returns the +// resulting canoto.Writer. Most users should just use MarshalCanoto. +// +// It is assumed that CalculateCanotoCache has been called since the last +// modification to this struct. +// +// It is assumed that this struct is ValidCanoto. +func (c *TransferResult) MarshalCanotoInto(w canoto.Writer) canoto.Writer { + if c == nil { + return w + } + if !canoto.IsZero(c.SenderBalance) { + canoto.Append(&w, canoto__TransferResult__SenderBalance__tag) + canoto.AppendUint(&w, c.SenderBalance) + } + if !canoto.IsZero(c.ReceiverBalance) { + canoto.Append(&w, canoto__TransferResult__ReceiverBalance__tag) + canoto.AppendUint(&w, c.ReceiverBalance) + } + return w +} diff --git a/examples/morpheusvm/actions/transfer.go b/examples/morpheusvm/actions/transfer.go index 952dad69a6..acf4090e33 100644 --- a/examples/morpheusvm/actions/transfer.go +++ b/examples/morpheusvm/actions/transfer.go @@ -3,13 +3,13 @@ package actions +//go:generate go run github.com/StephenButtolph/canoto/canoto $GOFILE + import ( "context" "errors" - "fmt" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils/wrappers" "github.com/ava-labs/hypersdk/chain" "github.com/ava-labs/hypersdk/codec" @@ -34,13 +34,15 @@ var ( type Transfer struct { // To is the recipient of the [Value]. - To codec.Address `serialize:"true" json:"to"` + To codec.Address `canoto:"fixed bytes,1" json:"to"` // Amount are transferred to [To]. - Value uint64 `serialize:"true" json:"value"` + Value uint64 `canoto:"uint,2" json:"value"` // Optional message to accompany transaction. - Memo []byte `serialize:"true" json:"memo"` + Memo []byte `canoto:"bytes,3" json:"memo"` + + canotoData canotoData_Transfer } func (*Transfer) GetTypeID() uint8 { @@ -55,42 +57,14 @@ func (t *Transfer) StateKeys(actor codec.Address, _ ids.ID) state.Keys { } func (t *Transfer) Bytes() []byte { - p := &wrappers.Packer{ - Bytes: make([]byte, 0, MaxMemoSize), - MaxSize: MaxTransferSize, - } - p.PackByte(mconsts.TransferID) - // XXX: AvalancheGo codec should never error for a valid value. Running e2e, we only - // interact with values unmarshalled from the network, which should guarantee a valid - // value here. - // Panic if we fail to marshal a value here to catch any potential bugs early. - // TODO: complete migration of user defined types to Canoto, so we do not need a panic - // here. - if err := codec.LinearCodec.MarshalInto(t, p); err != nil { - panic(err) - } - return p.Bytes + return append([]byte{mconsts.TransferID}, t.MarshalCanoto()...) } func UnmarshalTransfer(bytes []byte) (chain.Action, error) { t := &Transfer{} - if len(bytes) == 0 { - return nil, ErrUnmarshalEmptyTransfer - } - if bytes[0] != mconsts.TransferID { - return nil, fmt.Errorf("unexpected transfer typeID: %d != %d", bytes[0], mconsts.TransferID) - } - if err := codec.LinearCodec.UnmarshalFrom( - &wrappers.Packer{Bytes: bytes[1:]}, - t, - ); err != nil { + if err := t.UnmarshalCanoto(bytes[1:]); err != nil { return nil, err } - // Ensure that any parsed Transfer instance is valid - // and below MaxTransferSize - if len(t.Memo) > MaxMemoSize { - return nil, ErrOutputMemoTooLarge - } return t, nil } @@ -136,8 +110,10 @@ func (*Transfer) ValidRange(chain.Rules) (int64, int64) { var _ codec.Typed = (*TransferResult)(nil) type TransferResult struct { - SenderBalance uint64 `serialize:"true" json:"sender_balance"` - ReceiverBalance uint64 `serialize:"true" json:"receiver_balance"` + SenderBalance uint64 `canoto:"uint,1" json:"sender_balance"` + ReceiverBalance uint64 `canoto:"uint,2" json:"receiver_balance"` + + canotoData canotoData_TransferResult } func (*TransferResult) GetTypeID() uint8 { @@ -145,27 +121,12 @@ func (*TransferResult) GetTypeID() uint8 { } func (t *TransferResult) Bytes() []byte { - p := &wrappers.Packer{ - Bytes: make([]byte, 0, 256), - MaxSize: MaxTransferSize, - } - p.PackByte(mconsts.TransferID) - // XXX: AvalancheGo codec should never error for a valid value. Running e2e, we only - // interact with values unmarshalled from the network, which should guarantee a valid - // value here. - // Panic if we fail to marshal a value here to catch any potential bugs early. - // TODO: complete migration of user defined types to Canoto, so we do not need a panic - // here. - _ = codec.LinearCodec.MarshalInto(t, p) - return p.Bytes + return append([]byte{mconsts.TransferID}, t.MarshalCanoto()...) } func UnmarshalTransferResult(b []byte) (codec.Typed, error) { t := &TransferResult{} - if err := codec.LinearCodec.UnmarshalFrom( - &wrappers.Packer{Bytes: b[1:]}, // XXX: first byte is guaranteed to be the typeID by the type parser - t, - ); err != nil { + if err := t.UnmarshalCanoto(b[1:]); err != nil { return nil, err } return t, nil diff --git a/examples/morpheusvm/go.mod b/examples/morpheusvm/go.mod index df3d891e8a..b8f1066434 100644 --- a/examples/morpheusvm/go.mod +++ b/examples/morpheusvm/go.mod @@ -3,6 +3,7 @@ module github.com/ava-labs/hypersdk/examples/morpheusvm go 1.23.7 require ( + github.com/StephenButtolph/canoto v0.15.0 github.com/ava-labs/avalanchego v1.13.1-rc.0.0.20250414210208-c8b3f57d2a25 github.com/ava-labs/hypersdk v0.0.0-00010101000000-000000000000 github.com/fatih/color v1.13.0 @@ -16,7 +17,6 @@ require ( github.com/DataDog/zstd v1.5.2 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect - github.com/StephenButtolph/canoto v0.15.0 // indirect github.com/VictoriaMetrics/fastcache v1.12.1 // indirect github.com/ava-labs/coreth v0.15.1-rc.0 // indirect github.com/ava-labs/libevm v1.13.14-0.2.0.release // indirect diff --git a/examples/morpheusvm/tests/e2e/e2e_test.go b/examples/morpheusvm/tests/e2e/e2e_test.go index 3d1fe7f51b..0f13042cf6 100644 --- a/examples/morpheusvm/tests/e2e/e2e_test.go +++ b/examples/morpheusvm/tests/e2e/e2e_test.go @@ -49,16 +49,13 @@ var _ = ginkgo.SynchronizedBeforeSuite(func() []byte { testingNetworkConfig, err := workload.NewTestNetworkConfig(100 * time.Millisecond) require.NoError(err) - expectedABI, err := abi.NewABI(vm.ActionParser.GetRegisteredTypes(), vm.OutputParser.GetRegisteredTypes()) - require.NoError(err) - authFactories := testingNetworkConfig.AuthFactories() generator := workload.NewTxGenerator(authFactories[1]) he2e.SetWorkload( testingNetworkConfig, generator, - expectedABI, + abi.NewABI(vm.ActionParser, vm.OutputParser), authFactories[2], loadTxGenerators, hload.ShortBurstOrchestratorConfig{ diff --git a/examples/morpheusvm/vm/vm.go b/examples/morpheusvm/vm/vm.go index 95d0e4948d..556eb81879 100644 --- a/examples/morpheusvm/vm/vm.go +++ b/examples/morpheusvm/vm/vm.go @@ -18,9 +18,9 @@ import ( ) var ( - ActionParser *codec.TypeParser[chain.Action] - AuthParser *codec.TypeParser[chain.Auth] - OutputParser *codec.TypeParser[codec.Typed] + ActionParser *codec.CanotoParser[chain.Action] + AuthParser *codec.CanotoParser[chain.Auth] + OutputParser *codec.CanotoParser[codec.Typed] AuthProvider *auth.AuthProvider @@ -29,9 +29,9 @@ var ( // Setup types func init() { - ActionParser = codec.NewTypeParser[chain.Action]() - AuthParser = codec.NewTypeParser[chain.Auth]() - OutputParser = codec.NewTypeParser[codec.Typed]() + ActionParser = codec.NewCanotoParser[chain.Action]() + AuthParser = codec.NewCanotoParser[chain.Auth]() + OutputParser = codec.NewCanotoParser[codec.Typed]() AuthProvider = auth.NewAuthProvider() if err := auth.WithDefaultPrivateKeyFactories(AuthProvider); err != nil { @@ -40,7 +40,6 @@ func init() { if err := errors.Join( // When registering new actions, ALWAYS make sure to append at the end. - // Pass nil as second argument if manual marshalling isn't needed (if in doubt, you probably don't) ActionParser.Register(&actions.Transfer{}, actions.UnmarshalTransfer), // When registering new auth, ALWAYS make sure to append at the end. diff --git a/go.mod b/go.mod index 2abe10f85f..a6fc19b26c 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,6 @@ require ( golang.org/x/crypto v0.35.0 golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e golang.org/x/sync v0.11.0 - golang.org/x/text v0.22.0 google.golang.org/grpc v1.66.0 google.golang.org/protobuf v1.35.2 gopkg.in/yaml.v2 v2.4.0 @@ -151,6 +150,7 @@ require ( golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/term v0.29.0 // indirect + golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.29.0 // indirect gonum.org/v1/gonum v0.11.0 // indirect diff --git a/tests/e2e/e2e.go b/tests/e2e/e2e.go index 59c8f9cb51..0ec93fbed8 100644 --- a/tests/e2e/e2e.go +++ b/tests/e2e/e2e.go @@ -50,7 +50,7 @@ const ( var ( networkConfig workload.TestNetworkConfiguration txWorkload workload.TxWorkload - expectedABI abi.ABI + expectedABI *abi.ABI loadFactory chain.AuthFactory loadTxGenerator LoadTxGenerator @@ -78,7 +78,7 @@ type CreateTransfer func(to codec.Address, amount uint64, nonce uint64) chain.Ac func SetWorkload( networkConfigImpl workload.TestNetworkConfiguration, workloadTxGenerator workload.TxGenerator, - abi abi.ABI, + abi *abi.ABI, loadAccount chain.AuthFactory, generator LoadTxGenerator, shortBurstConf load.ShortBurstOrchestratorConfig, diff --git a/tests/workload/apis.go b/tests/workload/apis.go index d0038c867d..58692e0a19 100644 --- a/tests/workload/apis.go +++ b/tests/workload/apis.go @@ -34,14 +34,15 @@ func GetNetwork(ctx context.Context, require *require.Assertions, uris []string, } } -func GetABI(ctx context.Context, require *require.Assertions, uris []string, expectedABI abi.ABI) { +func GetABI(ctx context.Context, require *require.Assertions, uris []string, expectedABI *abi.ABI) { for _, uri := range uris { client := jsonrpc.NewJSONRPCClient(uri) actualABI, err := client.GetABI(ctx) require.NoError(err) - require.GreaterOrEqual(len(actualABI.Actions), 1) - require.NotEmpty(actualABI.Actions[0].Name) + require.GreaterOrEqual(len(actualABI.ActionTypes), 1) + require.NotEmpty(actualABI.ActionTypes[0].Name) + require.Equal(expectedABI, actualABI) } } diff --git a/vm/factory.go b/vm/factory.go index b3d5aaa93b..6037d6a2fc 100644 --- a/vm/factory.go +++ b/vm/factory.go @@ -14,9 +14,9 @@ type Factory struct { genesisFactory genesis.GenesisAndRuleFactory balanceHandler chain.BalanceHandler metadataManager chain.MetadataManager - actionCodec *codec.TypeParser[chain.Action] - authCodec *codec.TypeParser[chain.Auth] - outputCodec *codec.TypeParser[codec.Typed] + actionCodec *codec.CanotoParser[chain.Action] + authCodec *codec.CanotoParser[chain.Auth] + outputCodec *codec.CanotoParser[codec.Typed] authEngines auth.Engines options []Option @@ -26,9 +26,9 @@ func NewFactory( genesisFactory genesis.GenesisAndRuleFactory, balanceHandler chain.BalanceHandler, metadataManager chain.MetadataManager, - actionCodec *codec.TypeParser[chain.Action], - authCodec *codec.TypeParser[chain.Auth], - outputCodec *codec.TypeParser[codec.Typed], + actionCodec *codec.CanotoParser[chain.Action], + authCodec *codec.CanotoParser[chain.Auth], + outputCodec *codec.CanotoParser[codec.Typed], authEngines auth.Engines, options ...Option, ) *Factory { diff --git a/vm/resolutions.go b/vm/resolutions.go index 0ee606c6c0..668d090921 100644 --- a/vm/resolutions.go +++ b/vm/resolutions.go @@ -44,7 +44,7 @@ func (vm *VM) SubnetID() ids.ID { return vm.snowCtx.SubnetID } -func (vm *VM) GetABI() abi.ABI { +func (vm *VM) GetABI() *abi.ABI { return vm.abi } diff --git a/vm/vm.go b/vm/vm.go index a82f2e6b4b..aeaa3e76e2 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -105,10 +105,10 @@ type VM struct { balanceHandler chain.BalanceHandler metadataManager chain.MetadataManager txParser chain.Parser - abi abi.ABI - actionCodec *codec.TypeParser[chain.Action] - authCodec *codec.TypeParser[chain.Auth] - outputCodec *codec.TypeParser[codec.Typed] + abi *abi.ABI + actionCodec *codec.CanotoParser[chain.Action] + authCodec *codec.CanotoParser[chain.Auth] + outputCodec *codec.CanotoParser[codec.Typed] authEngines auth.Engines // authVerifiers are used to verify signatures in parallel @@ -127,9 +127,9 @@ func New( genesisFactory genesis.GenesisAndRuleFactory, balanceHandler chain.BalanceHandler, metadataManager chain.MetadataManager, - actionCodec *codec.TypeParser[chain.Action], - authCodec *codec.TypeParser[chain.Auth], - outputCodec *codec.TypeParser[codec.Typed], + actionCodec *codec.CanotoParser[chain.Action], + authCodec *codec.CanotoParser[chain.Auth], + outputCodec *codec.CanotoParser[codec.Typed], authEngines auth.Engines, options ...Option, ) (*VM, error) { @@ -140,16 +140,12 @@ func New( } allocatedNamespaces.Add(option.Namespace) } - abi, err := abi.NewABI(actionCodec.GetRegisteredTypes(), outputCodec.GetRegisteredTypes()) - if err != nil { - return nil, fmt.Errorf("failed to construct ABI: %w", err) - } return &VM{ balanceHandler: balanceHandler, metadataManager: metadataManager, txParser: chain.NewTxTypeParser(actionCodec, authCodec), - abi: abi, + abi: abi.NewABI(actionCodec, outputCodec), actionCodec: actionCodec, authCodec: authCodec, outputCodec: outputCodec, diff --git a/vm/vm_test.go b/vm/vm_test.go index cdd8e526df..5c94957c06 100644 --- a/vm/vm_test.go +++ b/vm/vm_test.go @@ -84,9 +84,9 @@ func WithConfigBytes(f func() []byte) func(*VMTestNetworkOptions) { func NewTestVMFactory(r *require.Assertions) *vm.Factory { var ( - actionParser = codec.NewTypeParser[chain.Action]() - authParser = codec.NewTypeParser[chain.Auth]() - outputParser = codec.NewTypeParser[codec.Typed]() + actionParser = codec.NewCanotoParser[chain.Action]() + authParser = codec.NewCanotoParser[chain.Auth]() + outputParser = codec.NewCanotoParser[codec.Typed]() ) r.NoError(errors.Join( actionParser.Register(&chaintest.TestAction{}, chaintest.UnmarshalTestAction),