From 87e4b14754e66e45c47e28251dc49ad4652b93db Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Mon, 8 Aug 2022 14:00:21 -0700 Subject: [PATCH 1/7] Add initial code --- avm-abi/encode.go | 601 +++++++++++++++++++ avm-abi/encode_test.go | 1251 ++++++++++++++++++++++++++++++++++++++++ avm-abi/json.go | 291 ++++++++++ avm-abi/json_test.go | 320 ++++++++++ avm-abi/type.go | 482 ++++++++++++++++ avm-abi/type_test.go | 591 +++++++++++++++++++ go.mod | 11 + go.sum | 17 + 8 files changed, 3564 insertions(+) create mode 100644 avm-abi/encode.go create mode 100644 avm-abi/encode_test.go create mode 100644 avm-abi/json.go create mode 100644 avm-abi/json_test.go create mode 100644 avm-abi/type.go create mode 100644 avm-abi/type_test.go create mode 100644 go.sum diff --git a/avm-abi/encode.go b/avm-abi/encode.go new file mode 100644 index 0000000..9d1b9d8 --- /dev/null +++ b/avm-abi/encode.go @@ -0,0 +1,601 @@ +package abi + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "math/big" + "reflect" + "strings" +) + +// typeCastToTuple cast an array-like ABI type into an ABI tuple type. +func (t Type) typeCastToTuple(tupLen ...int) (Type, error) { + var childT []Type + + switch t.abiTypeID { + case String: + if len(tupLen) != 1 { + return Type{}, fmt.Errorf("string type conversion to tuple need 1 length argument") + } + childT = make([]Type, tupLen[0]) + for i := 0; i < tupLen[0]; i++ { + childT[i] = byteType + } + case Address: + childT = make([]Type, addressByteSize) + for i := 0; i < addressByteSize; i++ { + childT[i] = byteType + } + case ArrayStatic: + childT = make([]Type, t.staticLength) + for i := 0; i < int(t.staticLength); i++ { + childT[i] = t.childTypes[0] + } + case ArrayDynamic: + if len(tupLen) != 1 { + return Type{}, fmt.Errorf("dynamic array type conversion to tuple need 1 length argument") + } + childT = make([]Type, tupLen[0]) + for i := 0; i < tupLen[0]; i++ { + childT[i] = t.childTypes[0] + } + default: + return Type{}, fmt.Errorf("type cannot support conversion to tuple") + } + + tuple, err := MakeTupleType(childT) + if err != nil { + return Type{}, err + } + return tuple, nil +} + +// Encode is an ABI type method to encode go values into bytes following ABI encoding rules +func (t Type) Encode(value interface{}) ([]byte, error) { + switch t.abiTypeID { + case Uint, Ufixed: + return encodeInt(value, t.bitSize) + case Bool: + boolValue, ok := value.(bool) + if !ok { + return nil, fmt.Errorf("cannot cast value to bool in bool encoding") + } + if boolValue { + return []byte{0x80}, nil + } + return []byte{0x00}, nil + case Byte: + byteValue, ok := value.(byte) + if !ok { + return nil, fmt.Errorf("cannot cast value to byte in byte encoding") + } + return []byte{byteValue}, nil + case ArrayStatic, Address: + castedType, err := t.typeCastToTuple() + if err != nil { + return nil, err + } + return castedType.Encode(value) + case ArrayDynamic: + dynamicArray, err := inferToSlice(value) + if err != nil { + return nil, err + } + castedType, err := t.typeCastToTuple(len(dynamicArray)) + if err != nil { + return nil, err + } + lengthEncode := make([]byte, lengthEncodeByteSize) + binary.BigEndian.PutUint16(lengthEncode, uint16(len(dynamicArray))) + encoded, err := castedType.Encode(value) + if err != nil { + return nil, err + } + encoded = append(lengthEncode, encoded...) + return encoded, nil + case String: + stringValue, okString := value.(string) + if !okString { + return nil, fmt.Errorf("cannot cast value to string or array dynamic in encoding") + } + byteValue := []byte(stringValue) + castedType, err := t.typeCastToTuple(len(byteValue)) + if err != nil { + return nil, err + } + lengthEncode := make([]byte, lengthEncodeByteSize) + binary.BigEndian.PutUint16(lengthEncode, uint16(len(byteValue))) + encoded, err := castedType.Encode(byteValue) + if err != nil { + return nil, err + } + encoded = append(lengthEncode, encoded...) + return encoded, nil + case Tuple: + return encodeTuple(value, t.childTypes) + default: + return nil, fmt.Errorf("cannot infer type for encoding") + } +} + +// encodeInt encodes int-alike golang values to bytes, following ABI encoding rules +func encodeInt(intValue interface{}, bitSize uint16) ([]byte, error) { + var bigInt *big.Int + + switch intValue := intValue.(type) { + case int8: + bigInt = big.NewInt(int64(intValue)) + case uint8: + bigInt = new(big.Int).SetUint64(uint64(intValue)) + case int16: + bigInt = big.NewInt(int64(intValue)) + case uint16: + bigInt = new(big.Int).SetUint64(uint64(intValue)) + case int32: + bigInt = big.NewInt(int64(intValue)) + case uint32: + bigInt = new(big.Int).SetUint64(uint64(intValue)) + case int64: + bigInt = big.NewInt(intValue) + case uint64: + bigInt = new(big.Int).SetUint64(intValue) + case uint: + bigInt = new(big.Int).SetUint64(uint64(intValue)) + case int: + bigInt = big.NewInt(int64(intValue)) + case *big.Int: + bigInt = intValue + default: + return nil, fmt.Errorf("cannot infer go type for uint encode") + } + + if bigInt.Sign() < 0 { + return nil, fmt.Errorf("passed in numeric value should be non negative") + } + + castedBytes := make([]byte, bitSize/8) + + if bigInt.Cmp(new(big.Int).Lsh(big.NewInt(1), uint(bitSize))) >= 0 { + return nil, fmt.Errorf("input value bit size %d > abi type bit size %d", bigInt.BitLen(), bitSize) + } + + bigInt.FillBytes(castedBytes) + return castedBytes, nil +} + +// inferToSlice infers an interface element to a slice of interface{}, returns error if it cannot infer successfully +func inferToSlice(value interface{}) ([]interface{}, error) { + reflectVal := reflect.ValueOf(value) + if reflectVal.Kind() != reflect.Slice && reflectVal.Kind() != reflect.Array { + return nil, fmt.Errorf("cannot infer an interface value as a slice of interface element") + } + // * if input is a slice, with nil, then reflectVal.Len() == 0 + // * if input is an array, it is not possible it is nil + values := make([]interface{}, reflectVal.Len()) + for i := 0; i < reflectVal.Len(); i++ { + values[i] = reflectVal.Index(i).Interface() + } + return values, nil +} + +// encodeTuple encodes slice-of-interface of golang values to bytes, following ABI encoding rules +func encodeTuple(value interface{}, childT []Type) ([]byte, error) { + if len(childT) >= abiEncodingLengthLimit { + return nil, fmt.Errorf("abi child type number exceeds uint16 maximum") + } + values, err := inferToSlice(value) + if err != nil { + return nil, err + } + if len(values) != len(childT) { + return nil, fmt.Errorf("cannot encode abi tuple: value slice length != child type number") + } + + // for each tuple element value, it has a head/tail component + // we create slots for head/tail bytes now, store them and concat them later + heads := make([][]byte, len(childT)) + tails := make([][]byte, len(childT)) + isDynamicIndex := make(map[int]bool) + + for i := 0; i < len(childT); i++ { + if childT[i].IsDynamic() { + // if it is a dynamic value, the head component is not pre-determined + // we store an empty placeholder first, since we will need it in byte length calculation + headsPlaceholder := []byte{0x00, 0x00} + heads[i] = headsPlaceholder + // we keep track that the index points to a dynamic value + isDynamicIndex[i] = true + tailEncoding, err := childT[i].Encode(values[i]) + if err != nil { + return nil, err + } + tails[i] = tailEncoding + isDynamicIndex[i] = true + } else if childT[i].abiTypeID == Bool { + // search previous bool + before := findBoolLR(childT, i, -1) + // search after bool + after := findBoolLR(childT, i, 1) + // append to heads and tails + if before%8 != 0 { + return nil, fmt.Errorf("cannot encode abi tuple: expected before has number of bool mod 8 == 0") + } + if after > 7 { + after = 7 + } + compressed, err := compressBools(values[i : i+after+1]) + if err != nil { + return nil, err + } + heads[i] = []byte{compressed} + i += after + isDynamicIndex[i] = false + } else { + encodeTi, err := childT[i].Encode(values[i]) + if err != nil { + return nil, err + } + heads[i] = encodeTi + isDynamicIndex[i] = false + } + } + + // adjust heads for dynamic type + // since head size can be pre-determined (for we are storing static value and dynamic value index in head) + // we accumulate the head size first + // (also note that though head size is pre-determined, head value is not necessarily pre-determined) + headLength := 0 + for _, headTi := range heads { + headLength += len(headTi) + } + + // when we iterate through the heads (byte slice), we need to find heads for dynamic values + // the head should correspond to the start index: len( head(x[1]) ... head(x[N]) tail(x[1]) ... tail(x[i-1]) ). + tailCurrLength := 0 + for i := 0; i < len(heads); i++ { + if isDynamicIndex[i] { + // calculate where the index of dynamic value encoding byte start + headValue := headLength + tailCurrLength + if headValue >= abiEncodingLengthLimit { + return nil, fmt.Errorf("cannot encode abi tuple: encode length exceeds uint16 maximum") + } + binary.BigEndian.PutUint16(heads[i], uint16(headValue)) + } + // accumulate the current tailing dynamic encoding bytes length. + tailCurrLength += len(tails[i]) + } + + // concat everything as the abi encoded bytes + encoded := make([]byte, 0, headLength+tailCurrLength) + for _, head := range heads { + encoded = append(encoded, head...) + } + for _, tail := range tails { + encoded = append(encoded, tail...) + } + return encoded, nil +} + +// compressBools takes a slice of interface{} (which can be casted to bools) length <= 8 +// and compress the bool values into a uint8 integer +func compressBools(boolSlice []interface{}) (uint8, error) { + var res uint8 = 0 + if len(boolSlice) > 8 { + return 0, fmt.Errorf("compressBools: cannot have slice length > 8") + } + for i := 0; i < len(boolSlice); i++ { + temp, ok := boolSlice[i].(bool) + if !ok { + return 0, fmt.Errorf("compressBools: cannot cast slice element to bool") + } + if temp { + res |= 1 << uint(7-i) + } + } + return res, nil +} + +// decodeUint decodes byte slice into golang int/big.Int +func decodeUint(encoded []byte, bitSize uint16) (interface{}, error) { + if len(encoded) != int(bitSize)/8 { + return nil, + fmt.Errorf("uint/ufixed decode: expected byte length %d, but got byte length %d", bitSize/8, len(encoded)) + } + switch bitSize / 8 { + case 1: + return encoded[0], nil + case 2: + return uint16(new(big.Int).SetBytes(encoded).Uint64()), nil + case 3, 4: + return uint32(new(big.Int).SetBytes(encoded).Uint64()), nil + case 5, 6, 7, 8: + return new(big.Int).SetBytes(encoded).Uint64(), nil + default: + return new(big.Int).SetBytes(encoded), nil + } +} + +// Decode is an ABI type method to decode bytes to go values from ABI encoding rules +func (t Type) Decode(encoded []byte) (interface{}, error) { + switch t.abiTypeID { + case Uint, Ufixed: + return decodeUint(encoded, t.bitSize) + case Bool: + if len(encoded) != 1 { + return nil, fmt.Errorf("boolean byte should be length 1 byte") + } + if encoded[0] == 0x00 { + return false, nil + } else if encoded[0] == 0x80 { + return true, nil + } + return nil, fmt.Errorf("single boolean encoded byte should be of form 0x80 or 0x00") + case Byte: + if len(encoded) != 1 { + return nil, fmt.Errorf("byte should be length 1") + } + return encoded[0], nil + case ArrayStatic: + castedType, err := t.typeCastToTuple() + if err != nil { + return nil, err + } + return castedType.Decode(encoded) + case Address: + if len(encoded) != addressByteSize { + return nil, fmt.Errorf("address should be length 32") + } + return encoded, nil + case ArrayDynamic: + if len(encoded) < lengthEncodeByteSize { + return nil, fmt.Errorf("dynamic array format corrupted") + } + dynamicLen := binary.BigEndian.Uint16(encoded[:lengthEncodeByteSize]) + castedType, err := t.typeCastToTuple(int(dynamicLen)) + if err != nil { + return nil, err + } + return castedType.Decode(encoded[lengthEncodeByteSize:]) + case String: + if len(encoded) < lengthEncodeByteSize { + return nil, fmt.Errorf("string format corrupted") + } + stringLenBytes := encoded[:lengthEncodeByteSize] + byteLen := binary.BigEndian.Uint16(stringLenBytes) + if len(encoded[lengthEncodeByteSize:]) != int(byteLen) { + return nil, fmt.Errorf("string representation in byte: length not matching") + } + return string(encoded[lengthEncodeByteSize:]), nil + case Tuple: + return decodeTuple(encoded, t.childTypes) + default: + return nil, fmt.Errorf("cannot infer type for decoding") + } +} + +// decodeTuple decodes byte slice with ABI type slice, outputting a slice of golang interface values +// following ABI encoding rules +func decodeTuple(encoded []byte, childT []Type) ([]interface{}, error) { + dynamicSegments := make([]int, 0, len(childT)+1) + valuePartition := make([][]byte, 0, len(childT)) + iterIndex := 0 + + for i := 0; i < len(childT); i++ { + if childT[i].IsDynamic() { + if len(encoded[iterIndex:]) < lengthEncodeByteSize { + return nil, fmt.Errorf("ill formed tuple dynamic typed value encoding") + } + dynamicIndex := binary.BigEndian.Uint16(encoded[iterIndex : iterIndex+lengthEncodeByteSize]) + dynamicSegments = append(dynamicSegments, int(dynamicIndex)) + valuePartition = append(valuePartition, nil) + iterIndex += lengthEncodeByteSize + } else if childT[i].abiTypeID == Bool { + // search previous bool + before := findBoolLR(childT, i, -1) + // search after bool + after := findBoolLR(childT, i, 1) + if before%8 == 0 { + if after > 7 { + after = 7 + } + // parse bool in a byte to multiple byte strings + for boolIndex := uint(0); boolIndex <= uint(after); boolIndex++ { + boolMask := 0x80 >> boolIndex + if encoded[iterIndex]&byte(boolMask) > 0 { + valuePartition = append(valuePartition, []byte{0x80}) + } else { + valuePartition = append(valuePartition, []byte{0x00}) + } + } + i += after + iterIndex++ + } else { + return nil, fmt.Errorf("expected before bool number mod 8 == 0") + } + } else { + // not bool ... + currLen, err := childT[i].ByteLen() + if err != nil { + return nil, err + } + valuePartition = append(valuePartition, encoded[iterIndex:iterIndex+currLen]) + iterIndex += currLen + } + if i != len(childT)-1 && iterIndex >= len(encoded) { + return nil, fmt.Errorf("input byte not enough to decode") + } + } + + if len(dynamicSegments) > 0 { + dynamicSegments = append(dynamicSegments, len(encoded)) + iterIndex = len(encoded) + } + if iterIndex < len(encoded) { + return nil, fmt.Errorf("input byte not fully consumed") + } + for i := 0; i < len(dynamicSegments)-1; i++ { + if dynamicSegments[i] > dynamicSegments[i+1] { + return nil, fmt.Errorf("dynamic segment should display a [l, r] space with l <= r") + } + } + + segIndex := 0 + for i := 0; i < len(childT); i++ { + if childT[i].IsDynamic() { + valuePartition[i] = encoded[dynamicSegments[segIndex]:dynamicSegments[segIndex+1]] + segIndex++ + } + } + + values := make([]interface{}, len(childT)) + for i := 0; i < len(childT); i++ { + var err error + values[i], err = childT[i].Decode(valuePartition[i]) + if err != nil { + return nil, err + } + } + return values, nil +} + +// maxAppArgs is the maximum number of arguments for an application call transaction, in compliance +// with ARC-4. Currently this is the same as the MaxAppArgs consensus parameter, but the +// difference is that the consensus parameter is liable to change in a future consensus upgrade. +// However, the ARC-4 ABI argument encoding **MUST** always remain the same. +const maxAppArgs = 16 + +// The tuple threshold is maxAppArgs, minus 1 for the method selector in the first app arg, +// minus 1 for the final app argument becoming a tuple of the remaining method args +const methodArgsTupleThreshold = maxAppArgs - 2 + +// ParseArgJSONtoByteSlice convert input method arguments to ABI encoded bytes +// it converts funcArgTypes into a tuple type and apply changes over input argument string (in JSON format) +// if there are greater or equal to 15 inputs, then we compact the tailing inputs into one tuple +func ParseArgJSONtoByteSlice(argTypes []string, jsonArgs []string, applicationArgs *[][]byte) error { + abiTypes := make([]Type, len(argTypes)) + for i, typeString := range argTypes { + abiType, err := TypeOf(typeString) + if err != nil { + return err + } + abiTypes[i] = abiType + } + + if len(abiTypes) != len(jsonArgs) { + return fmt.Errorf("input argument number %d != method argument number %d", len(jsonArgs), len(abiTypes)) + } + + // Up to 16 app arguments can be passed to app call. First is reserved for method selector, + // and the rest are for method call arguments. But if more than 15 method call arguments + // are present, then the method arguments after the 14th are placed in a tuple in the last + // app argument slot + if len(abiTypes) > maxAppArgs-1 { + typesForTuple := make([]Type, len(abiTypes)-methodArgsTupleThreshold) + copy(typesForTuple, abiTypes[methodArgsTupleThreshold:]) + + compactedType, err := MakeTupleType(typesForTuple) + if err != nil { + return err + } + + abiTypes = append(abiTypes[:methodArgsTupleThreshold], compactedType) + + tupleValues := make([]json.RawMessage, len(jsonArgs)-methodArgsTupleThreshold) + for i, jsonArg := range jsonArgs[methodArgsTupleThreshold:] { + tupleValues[i] = []byte(jsonArg) + } + + remainingJSON, err := json.Marshal(tupleValues) + if err != nil { + return err + } + + jsonArgs = append(jsonArgs[:methodArgsTupleThreshold], string(remainingJSON)) + } + + // parse JSON value to ABI encoded bytes + for i := 0; i < len(jsonArgs); i++ { + interfaceVal, err := abiTypes[i].UnmarshalFromJSON([]byte(jsonArgs[i])) + if err != nil { + return err + } + abiEncoded, err := abiTypes[i].Encode(interfaceVal) + if err != nil { + return err + } + *applicationArgs = append(*applicationArgs, abiEncoded) + } + return nil +} + +// ParseMethodSignature parses a method of format `method(argType1,argType2,...)retType` +// into `method` {`argType1`,`argType2`,...} and `retType` +func ParseMethodSignature(methodSig string) (name string, argTypes []string, returnType string, err error) { + argsStart := strings.Index(methodSig, "(") + if argsStart == -1 { + err = fmt.Errorf(`No parenthesis in method signature: "%s"`, methodSig) + return + } + + if argsStart == 0 { + err = fmt.Errorf(`Method signature has no name: "%s"`, methodSig) + return + } + + argsEnd := -1 + depth := 0 + for index, char := range methodSig { + if char == '(' { + depth++ + } else if char == ')' { + if depth == 0 { + err = fmt.Errorf(`Unpaired parenthesis in method signature: "%s"`, methodSig) + return + } + depth-- + if depth == 0 { + argsEnd = index + break + } + } + } + + if argsEnd == -1 { + err = fmt.Errorf(`Unpaired parenthesis in method signature: "%s"`, methodSig) + return + } + + name = methodSig[:argsStart] + argTypes, err = parseTupleContent(methodSig[argsStart+1 : argsEnd]) + returnType = methodSig[argsEnd+1:] + return +} + +// VerifyMethodSignature checks if a method signature and its referenced types can be parsed properly +func VerifyMethodSignature(methodSig string) error { + _, argTypes, retType, err := ParseMethodSignature(methodSig) + if err != nil { + return err + } + + for i, argType := range argTypes { + if IsReferenceType(argType) || IsTransactionType(argType) { + continue + } + + _, err = TypeOf(argType) + if err != nil { + return fmt.Errorf("Error parsing argument type at index %d: %s", i, err.Error()) + } + } + + if retType != VoidReturnType { + _, err = TypeOf(retType) + if err != nil { + return fmt.Errorf("Error parsing return type: %s", err.Error()) + } + } + + return nil +} diff --git a/avm-abi/encode_test.go b/avm-abi/encode_test.go new file mode 100644 index 0000000..9dbaa3c --- /dev/null +++ b/avm-abi/encode_test.go @@ -0,0 +1,1251 @@ +package abi + +import ( + "crypto/rand" + "encoding/binary" + "fmt" + "math/big" + "testing" + + "github.com/chrismcguire/gobberish" + "github.com/stretchr/testify/require" +) + +const ( + uintStepLength = 8 + uintBegin = 8 + uintEnd = 512 + uintRandomTestPoints = 1000 + uintTestCaseCount = 200 + ufixedPrecision = 160 + ufixedRandomTestPoints = 20 + tupleMaxLength = 10 + byteTestCaseCount = 1 << 8 + boolTestCaseCount = 2 + addressTestCaseCount = 300 + stringTestCaseCount = 10 + stringTestCaseSpecLenCount = 5 + takeNum = 10 + tupleTestCaseCount = 100 +) + +/* + The set of parameters ensure that the error of byte length >= 2^16 is eliminated. + + i. Consider uint512[] with length 10, the ABI encoding length is: 64 x 10 + 2 + (2 is introduced from dynamic array length encoding) + The motivation here is that, forall ABI type that is non-array/non-tuple like, + uint512 gives the longest byte length in ABI encoding + (utf-8 string's byte length is at most 42, address byte length is at most 32) + + ii. Consider a tuple of length 10, with all elements uint512[] of length 10. + The ABI encoding length is: 10 x 2 + 10 x 642 == 6440 + (2 is for tuple index to keep track of dynamic type encoding) + + iii. Consider a tuple of length 10, with all elements of tuples mentioned in (ii). + The ABI encoding length is: 10 x 2 + 10 x 6440 == 64420 + This is the end of the generation of nested-tuple test case, + no more layers of random tuples will be produced. + + This gives an upper bound for the produced ABI encoding byte length in this test script, + and noticing that length 64420 mentioned in (iii) is less than 2^16 == 65536. + Assuming that ABI implementation is correct, then the flaky test should not happen again. +*/ + +func TestEncodeValid(t *testing.T) { + // encoding test for uint type, iterating through all uint sizes + // randomly pick 1000 valid uint values and check if encoded value match with expected + for intSize := uintBegin; intSize <= uintEnd; intSize += uintStepLength { + upperLimit := new(big.Int).Lsh(big.NewInt(1), uint(intSize)) + uintType, err := makeUintType(intSize) + require.NoError(t, err, "make uint type fail") + + for i := 0; i < uintRandomTestPoints; i++ { + randomInt, err := rand.Int(rand.Reader, upperLimit) + require.NoError(t, err, "cryptographic random int init fail") + + expected := make([]byte, intSize/8) + randomInt.FillBytes(expected) + + uintEncode, err := uintType.Encode(randomInt) + require.NoError(t, err, "encoding from uint type fail") + + require.Equal(t, expected, uintEncode, "encode uint not match with expected") + } + // 2^[bitSize] - 1 test + // check if uint can contain max uint value (2^bitSize - 1) + largest := new(big.Int).Add( + upperLimit, + new(big.Int).Neg(big.NewInt(1)), + ) + encoded, err := uintType.Encode(largest) + require.NoError(t, err, "largest uint encode error") + require.Equal(t, largest.Bytes(), encoded, "encode uint largest do not match with expected") + } + + // encoding test for ufixed, iterating through all the valid ufixed bitSize and precision + // randomly generate 10 big int values for ufixed numerator and check if encoded value match with expected + // also check if ufixed can fit max numerator (2^bitSize - 1) under specific byte bitSize + for size := uintBegin; size <= uintEnd; size += uintStepLength { + upperLimit := new(big.Int).Lsh(big.NewInt(1), uint(size)) + largest := big.NewInt(0).Add( + upperLimit, + new(big.Int).Neg(big.NewInt(1)), + ) + for precision := 1; precision <= ufixedPrecision; precision++ { + typeUfixed, err := makeUfixedType(size, precision) + require.NoError(t, err, "make ufixed type fail") + + for i := 0; i < ufixedRandomTestPoints; i++ { + randomInt, err := rand.Int(rand.Reader, upperLimit) + require.NoError(t, err, "cryptographic random int init fail") + + encodedUfixed, err := typeUfixed.Encode(randomInt) + require.NoError(t, err, "ufixed encode fail") + + expected := make([]byte, size/8) + randomInt.FillBytes(expected) + require.Equal(t, expected, encodedUfixed, "encode ufixed not match with expected") + } + // (2^[bitSize] - 1) / (10^[precision]) test + ufixedLargestEncode, err := typeUfixed.Encode(largest) + require.NoError(t, err, "largest ufixed encode error") + require.Equal(t, largest.Bytes(), ufixedLargestEncode, + "encode ufixed largest do not match with expected") + } + } + + // encoding test for address, since address is 32 byte, it can be considered as 256 bit uint + // randomly generate 1000 uint256 and make address values, check if encoded value match with expected + upperLimit := new(big.Int).Lsh(big.NewInt(1), addressByteSize<<3) + for i := 0; i < uintRandomTestPoints; i++ { + randomAddrInt, err := rand.Int(rand.Reader, upperLimit) + require.NoError(t, err, "cryptographic random int init fail") + + addrBytesExpected := make([]byte, addressByteSize) + randomAddrInt.FillBytes(addrBytesExpected) + + addrBytesActual, err := addressType.Encode(addrBytesExpected) + require.NoError(t, err, "address encode fail") + require.Equal(t, addrBytesExpected, addrBytesActual, "encode addr not match with expected") + } + + // encoding test for bool values + for i := 0; i < boolTestCaseCount; i++ { + boolEncode, err := boolType.Encode(i == 1) + require.NoError(t, err, "bool encode fail") + expected := []byte{0x00} + if i == 1 { + expected = []byte{0x80} + } + require.Equal(t, expected, boolEncode, "encode bool not match with expected") + } + + // encoding test for byte values + for i := 0; i < byteTestCaseCount; i++ { + byteEncode, err := byteType.Encode(byte(i)) + require.NoError(t, err, "byte encode fail") + expected := []byte{byte(i)} + require.Equal(t, expected, byteEncode, "encode byte not match with expected") + } + + // encoding test for string values, since strings in ABI contain utf-8 symbols + // we use `gobberish` to generate random utf-8 symbols + // randomly generate utf-8 str from length 1 to 100, each length draw 10 random strs + // check if encoded ABI str match with expected value + for length := 1; length <= stringTestCaseCount; length++ { + for i := 0; i < stringTestCaseSpecLenCount; i++ { + // generate utf8 strings from `gobberish` at some length + utf8Str := gobberish.GenerateString(length) + // since string is just type alias of `byte[]`, we need to store number of bytes in encoding + utf8ByteLen := len([]byte(utf8Str)) + lengthBytes := make([]byte, 2) + binary.BigEndian.PutUint16(lengthBytes, uint16(utf8ByteLen)) + expected := append(lengthBytes, []byte(utf8Str)...) + + strEncode, err := stringType.Encode(utf8Str) + require.NoError(t, err, "string encode fail") + require.Equal(t, expected, strEncode, "encode string not match with expected") + } + } + + // encoding test for static bool array, the expected behavior of encoding is to + // compress multiple bool into a single byte. + // input: {T, F, F, T, T}, encode expected: {0b10011000} + staticBoolArrType := makeStaticArrayType(boolType, 5) + t.Run("static bool array encoding", func(t *testing.T) { + inputBase := []bool{true, false, false, true, true} + expected := []byte{ + 0b10011000, + } + boolArrEncode, err := staticBoolArrType.Encode(inputBase) + require.NoError(t, err, "static bool array encoding should not return error") + require.Equal(t, expected, boolArrEncode, "static bool array encode not match expected") + }) + + // encoding test for static bool array + // input: {F, F, F, T, T, F, T, F, T, F, T}, encode expected: {0b00011010, 0b10100000} + staticBoolArrType = makeStaticArrayType(boolType, 11) + t.Run("static bool array encoding", func(t *testing.T) { + inputBase := []bool{false, false, false, true, true, false, true, false, true, false, true} + expected := []byte{ + 0b00011010, 0b10100000, + } + boolArrEncode, err := staticBoolArrType.Encode(inputBase) + require.NoError(t, err, "static bool array encoding should not return error") + require.Equal(t, expected, boolArrEncode, "static bool array encode not match expected") + }) + + // encoding test for dynamic bool array + // input: {F, T, F, T, F, T, F, T, F, T}, encode expected: {0b01010101, 0b01000000} + dynamicBoolArrayType := makeDynamicArrayType(boolType) + t.Run("dynamic bool array encoding", func(t *testing.T) { + inputBase := []bool{false, true, false, true, false, true, false, true, false, true} + expected := []byte{ + 0x00, 0x0A, 0b01010101, 0b01000000, + } + boolArrEncode, err := dynamicBoolArrayType.Encode(inputBase) + require.NoError(t, err, "dynamic bool array encoding should not return error") + require.Equal(t, expected, boolArrEncode, "dynamic bool array encode not match expected") + }) + + // encoding test for dynamic tuple values + // input type: (string, bool, bool, bool, bool, string) + // input value: ("ABC", T, F, T, F, "DEF") + /* + encode expected: + 0x00, 0x05 (first string start at 5th byte) + 0b10100000 (4 bool tuple element compacted together) + 0x00, 0x0A (second string start at 10th byte) + 0x00, 0x03 (first string byte length 3) + byte('A'), byte('B'), byte('C') (first string encoded bytes) + 0x00, 0x03 (second string byte length 3) + byte('D'), byte('E'), byte('F') (second string encoded bytes) + */ + tupleType, err := TypeOf("(string,bool,bool,bool,bool,string)") + require.NoError(t, err, "type from string for dynamic tuple type should not return error") + t.Run("dynamic tuple encoding", func(t *testing.T) { + inputBase := []interface{}{ + "ABC", true, false, true, false, "DEF", + } + expected := []byte{ + 0x00, 0x05, 0b10100000, 0x00, 0x0A, + 0x00, 0x03, byte('A'), byte('B'), byte('C'), + 0x00, 0x03, byte('D'), byte('E'), byte('F'), + } + stringTupleEncode, err := tupleType.Encode(inputBase) + require.NoError(t, err, "string tuple encoding should not return error") + require.Equal(t, expected, stringTupleEncode, "string tuple encoding not match expected") + }) + + // encoding test for tuples with static bool arrays + // input type: {bool[2], bool[2]} + // input value: ({T, T}, {T, T}) + /* + encode expected: + 0b11000000 (first static bool array) + 0b11000000 (second static bool array) + */ + tupleType, err = TypeOf("(bool[2],bool[2])") + require.NoError(t, err, "type from string for tuple type should not return error") + t.Run("static bool array tuple encoding", func(t *testing.T) { + expected := []byte{ + 0b11000000, + 0b11000000, + } + actual, err := tupleType.Encode([]interface{}{ + []bool{true, true}, + []bool{true, true}, + }) + require.NoError(t, err, "encode tuple value should not return error") + require.Equal(t, expected, actual, "encode static bool tuple should be equal") + }) + + // encoding test for tuples with static and dynamic bool arrays + // input type: (bool[2], bool[]) + // input value: ({T, T}, {T, T}) + /* + encode expected: + 0b11000000 (first static bool array) + 0x00, 0x03 (second dynamic bool array starts at 3rd byte) + 0x00, 0x02 (dynamic bool array length 2) + 0b11000000 (second static bool array) + */ + tupleType, err = TypeOf("(bool[2],bool[])") + require.NoError(t, err, "type from string for tuple type should not return error") + t.Run("static/dynamic bool array tuple encoding", func(t *testing.T) { + expected := []byte{ + 0b11000000, + 0x00, 0x03, + 0x00, 0x02, 0b11000000, + } + actual, err := tupleType.Encode([]interface{}{ + []bool{true, true}, + []bool{true, true}, + }) + require.NoError(t, err, "tuple value encoding should not return error") + require.Equal(t, expected, actual, "encode static/dynamic bool array tuple should not return error") + }) + + // encoding test for tuples with all dynamic bool arrays + // input type: (bool[], bool[]) + // input values: ({}, {}) + /* + encode expected: + 0x00, 0x04 (first dynamic bool array starts at 4th byte) + 0x00, 0x06 (second dynamic bool array starts at 6th byte) + 0x00, 0x00 (first dynamic bool array length 0) + 0x00, 0x00 (second dynamic bool array length 0) + */ + tupleType, err = TypeOf("(bool[],bool[])") + require.NoError(t, err, "type from string for tuple type should not return error") + t.Run("empty dynamic array tuple encoding", func(t *testing.T) { + expected := []byte{ + 0x00, 0x04, 0x00, 0x06, + 0x00, 0x00, 0x00, 0x00, + } + actual, err := tupleType.Encode([]interface{}{ + []bool{}, []bool{}, + }) + require.NoError(t, err, "encode empty dynamic array tuple should not return error") + require.Equal(t, expected, actual, "encode empty dynamic array tuple does not match with expected") + }) + + // encoding test for empty tuple + // input: (), expected encoding: "" + tupleType, err = TypeOf("()") + require.NoError(t, err, "type from string for tuple type should not return error") + t.Run("empty tuple encoding", func(t *testing.T) { + expected := make([]byte, 0) + actual, err := tupleType.Encode([]interface{}{}) + require.NoError(t, err, "encode empty tuple should not return error") + require.Equal(t, expected, actual, "empty tuple encode should not return error") + }) +} + +func TestDecodeValid(t *testing.T) { + // decoding test for uint, iterating through all valid uint bitSize + // randomly take 1000 tests on each valid bitSize + // generate bytes from random uint values and decode bytes with additional type information + for intSize := uintBegin; intSize <= uintEnd; intSize += uintStepLength { + upperLimit := new(big.Int).Lsh(big.NewInt(1), uint(intSize)) + uintType, err := makeUintType(intSize) + require.NoError(t, err, "make uint type failure") + for i := 0; i < uintRandomTestPoints; i++ { + randBig, err := rand.Int(rand.Reader, upperLimit) + require.NoError(t, err, "cryptographic random int init fail") + + var expected interface{} + if intSize <= 64 && intSize > 32 { + expected = randBig.Uint64() + } else if intSize <= 32 && intSize > 16 { + expected = uint32(randBig.Uint64()) + } else if intSize == 16 { + expected = uint16(randBig.Uint64()) + } else if intSize == 8 { + expected = uint8(randBig.Uint64()) + } else { + expected = randBig + } + + encodedUint, err := uintType.Encode(expected) + require.NoError(t, err, "uint encode fail") + + actual, err := uintType.Decode(encodedUint) + require.NoError(t, err, "decoding uint should not return error") + require.Equal(t, expected, actual, "decode uint fail to match expected value") + } + } + + // decoding test for ufixed, iterating through all valid ufixed bitSize and precision + // randomly take 10 tests on each valid setting + // generate ufixed bytes and try to decode back with additional type information + for size := uintBegin; size <= uintEnd; size += uintStepLength { + upperLimit := big.NewInt(0).Lsh(big.NewInt(1), uint(size)) + for precision := 1; precision <= ufixedPrecision; precision++ { + ufixedType, err := makeUfixedType(size, precision) + require.NoError(t, err, "make ufixed type failure") + for i := 0; i < ufixedRandomTestPoints; i++ { + randBig, err := rand.Int(rand.Reader, upperLimit) + require.NoError(t, err, "cryptographic random int init fail") + + var expected interface{} + if size <= 64 && size > 32 { + expected = randBig.Uint64() + } else if size <= 32 && size > 16 { + expected = uint32(randBig.Uint64()) + } else if size == 16 { + expected = uint16(randBig.Uint64()) + } else if size == 8 { + expected = uint8(randBig.Uint64()) + } else { + expected = randBig + } + + encodedUfixed, err := ufixedType.Encode(expected) + require.NoError(t, err, "ufixed encode fail") + require.NoError(t, err, "cast big integer to expected value should not return error") + + actual, err := ufixedType.Decode(encodedUfixed) + require.NoError(t, err, "decoding ufixed should not return error") + require.Equal(t, expected, actual, "decode ufixed fail to match expected value") + } + } + } + + // decoding test for address, randomly take 300 tests + // address is type alias of byte[32], we generate address value with random 256 bit big int values + // we make the expected address value and decode the encoding of expected, check if they match + upperLimit := new(big.Int).Lsh(big.NewInt(1), addressByteSize<<3) + for i := 0; i < addressTestCaseCount; i++ { + randomAddrInt, err := rand.Int(rand.Reader, upperLimit) + require.NoError(t, err, "cryptographic random int init fail") + + expected := make([]byte, addressByteSize) + randomAddrInt.FillBytes(expected) + + actual, err := addressType.Decode(expected) + require.NoError(t, err, "decoding address should not return error") + require.Equal(t, expected, actual, "decode addr not match with expected") + } + + // bool value decoding test + for i := 0; i < 2; i++ { + boolEncode, err := boolType.Encode(i == 1) + require.NoError(t, err, "bool encode fail") + actual, err := boolType.Decode(boolEncode) + require.NoError(t, err, "decoding bool should not return error") + require.Equal(t, i == 1, actual, "decode bool not match with expected") + } + + // byte value decoding test, iterating through 256 valid byte value + for i := 0; i < byteTestCaseCount; i++ { + byteEncode, err := byteType.Encode(byte(i)) + require.NoError(t, err, "byte encode fail") + actual, err := byteType.Decode(byteEncode) + require.NoError(t, err, "decoding byte should not return error") + require.Equal(t, byte(i), actual, "decode byte not match with expected") + } + + // string value decoding test, test from utf string length 1 to 10 + // randomly take 5 utf-8 strings to make ABI string values + // decode the encoded expected value and check if they match + for length := 1; length <= stringTestCaseCount; length++ { + for i := 0; i < stringTestCaseSpecLenCount; i++ { + expected := gobberish.GenerateString(length) + strEncode, err := stringType.Encode(expected) + require.NoError(t, err, "string encode fail") + actual, err := stringType.Decode(strEncode) + require.NoError(t, err, "decoding string should not return error") + require.Equal(t, expected, actual, "encode string not match with expected") + } + } + + // decoding test for static bool array + // expected value: bool[5]: {T, F, F, T, T} + // input: 0b10011000 + t.Run("static bool array decode", func(t *testing.T) { + staticBoolArrT, err := TypeOf("bool[5]") + require.NoError(t, err, "make static bool array type failure") + expected := []interface{}{true, false, false, true, true} + actual, err := staticBoolArrT.Decode([]byte{0b10011000}) + require.NoError(t, err, "decoding static bool array should not return error") + require.Equal(t, expected, actual, "static bool array decode do not match expected") + }) + + // decoding test for static bool array + // expected value: bool[11]: F, F, F, T, T, F, T, F, T, F, T + // input: 0b00011010, 0b10100000 + t.Run("static bool array decode", func(t *testing.T) { + staticBoolArrT, err := TypeOf("bool[11]") + require.NoError(t, err, "make static bool array type failure") + expected := []interface{}{false, false, false, true, true, false, true, false, true, false, true} + actual, err := staticBoolArrT.Decode([]byte{0b00011010, 0b10100000}) + require.NoError(t, err, "decoding static bool array should not return error") + require.Equal(t, expected, actual, "static bool array decode do not match expected") + }) + + // decoding test for static uint array + // expected input: uint64[8]: {1, 2, 3, 4, 5, 6, 7, 8} + /* + input: 0, 0, 0, 0, 0, 0, 0, 1 (encoding for uint64 1) + 0, 0, 0, 0, 0, 0, 0, 2 (encoding for uint64 2) + 0, 0, 0, 0, 0, 0, 0, 3 (encoding for uint64 3) + 0, 0, 0, 0, 0, 0, 0, 4 (encoding for uint64 4) + 0, 0, 0, 0, 0, 0, 0, 5 (encoding for uint64 5) + 0, 0, 0, 0, 0, 0, 0, 6 (encoding for uint64 6) + 0, 0, 0, 0, 0, 0, 0, 7 (encoding for uint64 7) + 0, 0, 0, 0, 0, 0, 0, 8 (encoding for uint64 8) + */ + t.Run("static uint array decode", func(t *testing.T) { + staticUintArrT, err := TypeOf("uint64[8]") + require.NoError(t, err, "make static uint array type failure") + expected := []interface{}{ + uint64(1), uint64(2), + uint64(3), uint64(4), + uint64(5), uint64(6), + uint64(7), uint64(8), + } + arrayEncoded, err := staticUintArrT.Encode(expected) + require.NoError(t, err, "uint64 static array encode should not return error") + actual, err := staticUintArrT.Decode(arrayEncoded) + require.NoError(t, err, "uint64 static array decode should not return error") + require.Equal(t, expected, actual, "uint64 static array decode do not match with expected value") + }) + + // decoding test for dynamic bool array + // expected value: bool[]: {F, T, F, T, F, T, F, T, F, T} + /* + input bytes: 0x00, 0x0A (dynamic bool array length 10) + 0b01010101, 0b01000000 (dynamic bool array encoding) + */ + t.Run("dynamic bool array decode", func(t *testing.T) { + dynamicBoolArrT, err := TypeOf("bool[]") + require.NoError(t, err, "make dynamic bool array type failure") + expected := []interface{}{false, true, false, true, false, true, false, true, false, true} + inputEncoded := []byte{ + 0x00, 0x0A, 0b01010101, 0b01000000, + } + actual, err := dynamicBoolArrT.Decode(inputEncoded) + require.NoError(t, err, "decode dynamic array should not return error") + require.Equal(t, expected, actual, "decode dynamic array do not match expected") + }) + + // decoding test for dynamic tuple values + // expected value type: (string, bool, bool, bool, bool, string) + // expected value: ("ABC", T, F, T, F, "DEF") + /* + input bytes: + 0x00, 0x05 (first string start at 5th byte) + 0b10100000 (4 bool tuple element compacted together) + 0x00, 0x0A (second string start at 10th byte) + 0x00, 0x03 (first string byte length 3) + byte('A'), byte('B'), byte('C') (first string encoded bytes) + 0x00, 0x03 (second string byte length 3) + byte('D'), byte('E'), byte('F') (second string encoded bytes) + */ + t.Run("dynamic tuple decoding", func(t *testing.T) { + tupleT, err := TypeOf("(string,bool,bool,bool,bool,string)") + require.NoError(t, err, "make tuple type failure") + inputEncode := []byte{ + 0x00, 0x05, 0b10100000, 0x00, 0x0A, + 0x00, 0x03, byte('A'), byte('B'), byte('C'), + 0x00, 0x03, byte('D'), byte('E'), byte('F'), + } + expected := []interface{}{ + "ABC", true, false, true, false, "DEF", + } + actual, err := tupleT.Decode(inputEncode) + require.NoError(t, err, "decoding dynamic tuple should not return error") + require.Equal(t, expected, actual, "dynamic tuple not match with expected") + }) + + // decoding test for tuple with static bool array + // expected type: (bool[2], bool[2]) + // expected value: ({T, T}, {T, T}) + /* + input bytes: + 0b11000000 (first static bool array) + 0b11000000 (second static bool array) + */ + t.Run("static bool array tuple decoding", func(t *testing.T) { + tupleT, err := TypeOf("(bool[2],bool[2])") + require.NoError(t, err, "make tuple type failure") + expected := []interface{}{ + []interface{}{true, true}, + []interface{}{true, true}, + } + encodedInput := []byte{ + 0b11000000, + 0b11000000, + } + actual, err := tupleT.Decode(encodedInput) + require.NoError(t, err, "decode tuple value should not return error") + require.Equal(t, expected, actual, "decoded tuple value do not match with expected") + }) + + // decoding test for tuple with static and dynamic bool array + // expected type: (bool[2], bool[]) + // expected value: ({T, T}, {T, T}) + /* + input bytes: + 0b11000000 (first static bool array) + 0x00, 0x03 (second dynamic bool array starts at 3rd byte) + 0x00, 0x02 (dynamic bool array length 2) + 0b11000000 (second static bool array) + */ + t.Run("static/dynamic bool array tuple decoding", func(t *testing.T) { + tupleT, err := TypeOf("(bool[2],bool[])") + require.NoError(t, err, "make tuple type failure") + expected := []interface{}{ + []interface{}{true, true}, + []interface{}{true, true}, + } + encodedInput := []byte{ + 0b11000000, + 0x00, 0x03, + 0x00, 0x02, 0b11000000, + } + actual, err := tupleT.Decode(encodedInput) + require.NoError(t, err, "decode tuple for static/dynamic bool array should not return error") + require.Equal(t, expected, actual, "decoded tuple value do not match with expected") + }) + + // decoding test for tuple with all dynamic bool array + // expected value: (bool[], bool[]) + // expected value: ({}, {}) + /* + input bytes: + 0x00, 0x04 (first dynamic bool array starts at 4th byte) + 0x00, 0x06 (second dynamic bool array starts at 6th byte) + 0x00, 0x00 (first dynamic bool array length 0) + 0x00, 0x00 (second dynamic bool array length 0) + */ + t.Run("empty dynamic array tuple decoding", func(t *testing.T) { + tupleT, err := TypeOf("(bool[],bool[])") + require.NoError(t, err, "make tuple type failure") + expected := []interface{}{ + []interface{}{}, []interface{}{}, + } + encodedInput := []byte{ + 0x00, 0x04, 0x00, 0x06, + 0x00, 0x00, 0x00, 0x00, + } + actual, err := tupleT.Decode(encodedInput) + require.NoError(t, err, "decode tuple for empty dynamic array should not return error") + require.Equal(t, expected, actual, "decoded tuple value do not match with expected") + }) + + // decoding test for empty tuple + // expected value: () + // byte input: "" + t.Run("empty tuple decoding", func(t *testing.T) { + tupleT, err := TypeOf("()") + require.NoError(t, err, "make empty tuple type should not return error") + actual, err := tupleT.Decode([]byte{}) + require.NoError(t, err, "decode empty tuple should not return error") + require.Equal(t, []interface{}{}, actual, "empty tuple encode should not return error") + }) +} + +func TestDecodeInvalid(t *testing.T) { + // decoding test for *corrupted* static bool array + // expected 9 elements for static bool array + // encoded bytes have only 8 bool values + // should throw error + t.Run("corrupted static bool array decode", func(t *testing.T) { + inputBase := []byte{0b11111111} + arrayType := makeStaticArrayType(boolType, 9) + _, err := arrayType.Decode(inputBase) + require.Error(t, err, "decoding corrupted static bool array should return error") + }) + + // decoding test for *corrupted* static bool array + // expected 8 elements for static bool array + // encoded bytes have 1 byte more (0b00000000) + // should throw error + t.Run("corrupted static bool array decode", func(t *testing.T) { + inputBase := []byte{0b01001011, 0b00000000} + arrayType := makeStaticArrayType(boolType, 8) + _, err := arrayType.Decode(inputBase) + require.Error(t, err, "decoding corrupted static bool array should return error") + }) + + // decoding test for *corrupted* static uint array + // expected 8 uint elements in static uint64[8] array + // encoded bytes provide only 7 uint64 encoding + // should throw error + t.Run("static uint array decode", func(t *testing.T) { + inputBase := []byte{ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 2, + 0, 0, 0, 0, 0, 0, 0, 3, + 0, 0, 0, 0, 0, 0, 0, 4, + 0, 0, 0, 0, 0, 0, 0, 5, + 0, 0, 0, 0, 0, 0, 0, 6, + } + uintTArray, err := TypeOf("uint64[8]") + require.NoError(t, err, "make uint64 static array type should not return error") + _, err = uintTArray.Decode(inputBase) + require.Error(t, err, "corrupted uint64 static array decode should return error") + }) + + // decoding test for *corrupted* static uint array + // expected 7 uint elements in static uint64[7] array + // encoded bytes provide 8 uint64 encoding (one more uint64: 7) + // should throw error + t.Run("static uint array decode", func(t *testing.T) { + inputBase := []byte{ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 2, + 0, 0, 0, 0, 0, 0, 0, 3, + 0, 0, 0, 0, 0, 0, 0, 4, + 0, 0, 0, 0, 0, 0, 0, 5, + 0, 0, 0, 0, 0, 0, 0, 6, + 0, 0, 0, 0, 0, 0, 0, 7, + } + uintTArray, err := TypeOf("uint64[7]") + require.NoError(t, err, "make uint64 static array type should not return error") + _, err = uintTArray.Decode(inputBase) + require.Error(t, err, "corrupted uint64 static array decode should return error") + }) + + // decoding test for *corrupted* dynamic bool array + // expected 0x0A (10) bool elements in encoding head + // encoded bytes provide only 8 bool elements + // should throw error + t.Run("corrupted dynamic bool array decode", func(t *testing.T) { + inputBase := []byte{ + 0x00, 0x0A, 0b10101010, + } + dynamicT := makeDynamicArrayType(boolType) + _, err := dynamicT.Decode(inputBase) + require.Error(t, err, "decode corrupted dynamic array should return error") + }) + + // decoding test for *corrupted* dynamic bool array + // expected 0x07 (7) bool elements in encoding head + // encoded bytes provide 1 byte more (0b00000000) + // should throw error + t.Run("corrupted dynamic bool array decode", func(t *testing.T) { + inputBase := []byte{ + 0x00, 0x07, 0b10101010, 0b00000000, + } + dynamicT := makeDynamicArrayType(boolType) + _, err := dynamicT.Decode(inputBase) + require.Error(t, err, "decode corrupted dynamic array should return error") + }) + + // decoding test for *corrupted* dynamic tuple value + // expected type: (string, bool, bool, bool, bool, string) + // expected value: ("ABC", T, F, T, F, "DEF") + /* + corrupted bytes: + 0x00, 0x04 (corrupted: first string start at 4th byte, should be 5th) + 0b10100000 (4 bool tuple element compacted together) + 0x00, 0x0A (second string start at 10th byte) + 0x00, 0x03 (first string byte length 3) + byte('A'), byte('B'), byte('C') (first string encoded bytes) + 0x00, 0x03 (second string byte length 3) + byte('D'), byte('E'), byte('F') (second string encoded bytes) + */ + // the result would be: first string have length 0x0A, 0x00 + // the length exceeds the segment it allocated: 0x0A, 0x00, 0x03, byte('A'), byte('B'), byte('C') + // should throw error + t.Run("corrupted dynamic tuple decoding", func(t *testing.T) { + inputEncode := []byte{ + 0x00, 0x04, 0b10100000, 0x00, 0x0A, + 0x00, 0x03, byte('A'), byte('B'), byte('C'), + 0x00, 0x03, byte('D'), byte('E'), byte('F'), + } + tupleT, err := TypeOf("(string,bool,bool,bool,bool,string)") + require.NoError(t, err, "make tuple type failure") + _, err = tupleT.Decode(inputEncode) + require.Error(t, err, "corrupted decoding dynamic tuple should return error") + }) + + // decoding test for *corrupted* tuple with static bool arrays + // expected type: (bool[2], bool[2]) + // expected value: ({T, T}, {T, T}) + /* + corrupted bytes test case 0: + 0b11000000 + 0b11000000 + 0b00000000 <- corrupted byte, 1 byte more + + corrupted bytes test case 0: + 0b11000000 + <- corrupted byte, 1 byte missing + */ + t.Run("corrupted static bool array tuple decoding", func(t *testing.T) { + expectedType, err := TypeOf("(bool[2],bool[2])") + require.NoError(t, err, "make tuple type failure") + encodedInput0 := []byte{ + 0b11000000, + 0b11000000, + 0b00000000, + } + _, err = expectedType.Decode(encodedInput0) + require.Error(t, err, "decode corrupted tuple value should return error") + + encodedInput1 := []byte{ + 0b11000000, + } + _, err = expectedType.Decode(encodedInput1) + require.Error(t, err, "decode corrupted tuple value should return error") + }) + + // decoding test for *corrupted* tuple with static and dynamic bool array + // expected type: (bool[2], bool[]) + // expected value: ({T, T}, {T, T}) + /* + corrupted bytes: + 0b11000000 (first static bool array) + 0x03 <- corrupted, missing 0x00 byte (second dynamic bool array starts at 3rd byte) + 0x00, 0x02 (dynamic bool array length 2) + 0b11000000 (second static bool array) + */ + t.Run("corrupted static/dynamic bool array tuple decoding", func(t *testing.T) { + encodedInput := []byte{ + 0b11000000, + 0x03, + 0x00, 0x02, 0b11000000, + } + tupleT, err := TypeOf("(bool[2],bool[])") + require.NoError(t, err, "make tuple type failure") + _, err = tupleT.Decode(encodedInput) + require.Error(t, err, "decode corrupted tuple for static/dynamic bool array should return error") + }) + + // decoding test for *corrupted* tuple with dynamic bool array + // expected type: (bool[], bool[]) + // expected value: ({}, {}) + /* + corrupted bytes: + 0x00, 0x04 (first dynamic bool array starts at 4th byte) + 0x00, 0x07 <- corrupted, should be 0x06 (second dynamic bool array starts at 6th byte) + 0x00, 0x00 (first dynamic bool array length 0) + 0x00, 0x00 (second dynamic bool array length 0) + + first dynamic array starts at 0x04, segment is 0x00, 0x00, 0x00, 1 byte 0x00 more + second dynamic array starts at 0x07, and only have 0x00 1 byte + */ + // should return error + t.Run("corrupted empty dynamic array tuple decoding", func(t *testing.T) { + encodedInput := []byte{ + 0x00, 0x04, 0x00, 0x07, + 0x00, 0x00, 0x00, 0x00, + } + tupleT, err := TypeOf("(bool[],bool[])") + require.NoError(t, err, "make tuple type failure") + _, err = tupleT.Decode(encodedInput) + require.Error(t, err, "decode corrupted tuple for empty dynamic array should return error") + }) + + // decoding test for *corrupted* empty tuple + // expected value: () + // corrupted input: 0xFF, should be empty byte + // should return error + t.Run("corrupted empty tuple decoding", func(t *testing.T) { + encodedInput := []byte{0xFF} + tupleT, err := TypeOf("()") + require.NoError(t, err, "make tuple type failure") + _, err = tupleT.Decode(encodedInput) + require.Error(t, err, "decode corrupted empty tuple should return error") + }) +} + +type testUnit struct { + serializedType string + value interface{} +} + +func categorySelfRoundTripTest(t *testing.T, category []testUnit) { + for _, testObj := range category { + abiType, err := TypeOf(testObj.serializedType) + require.NoError(t, err, "failure to deserialize type: "+testObj.serializedType) + encodedValue, err := abiType.Encode(testObj.value) + require.NoError(t, err, + "failure to encode value %#v over type %s", testObj.value, testObj.serializedType, + ) + actual, err := abiType.Decode(encodedValue) + require.NoError(t, err, + "failure to decode value %#v for type %s", encodedValue, testObj.serializedType, + ) + require.Equal(t, testObj.value, actual, + "decoded value %#v not equal to expected value %#v", actual, testObj.value, + ) + jsonEncodedValue, err := abiType.MarshalToJSON(testObj.value) + require.NoError(t, err, + "failure to encode value %#v to JSON type", testObj.value, + ) + jsonActual, err := abiType.UnmarshalFromJSON(jsonEncodedValue) + require.NoError(t, err, + "failure to decode JSON value %s back for type %s", + string(jsonEncodedValue), testObj.serializedType, + ) + require.Equal(t, testObj.value, jsonActual, + "decode JSON value %s not equal to expected %s", jsonActual, testObj.value, + ) + } +} + +func addPrimitiveRandomValues(t *testing.T, pool *map[BaseType][]testUnit) { + (*pool)[Uint] = make([]testUnit, uintTestCaseCount*uintEnd/uintStepLength) + (*pool)[Ufixed] = make([]testUnit, ufixedPrecision*uintEnd/uintStepLength) + + uintIndex := 0 + ufixedIndex := 0 + + for bitSize := uintBegin; bitSize <= uintEnd; bitSize += uintStepLength { + max := new(big.Int).Lsh(big.NewInt(1), uint(bitSize)) + + uintT, err := makeUintType(bitSize) + require.NoError(t, err, "make uint type failure") + uintTstr := uintT.String() + + for j := 0; j < uintTestCaseCount; j++ { + randVal, err := rand.Int(rand.Reader, max) + require.NoError(t, err, "generate random uint, should be no error") + + narrowest, err := castBigIntToNearestPrimitive(randVal, uint16(bitSize)) + require.NoError(t, err, "cast random uint to nearest primitive failure") + + (*pool)[Uint][uintIndex] = testUnit{serializedType: uintTstr, value: narrowest} + uintIndex++ + } + + for precision := 1; precision <= ufixedPrecision; precision++ { + randVal, err := rand.Int(rand.Reader, max) + require.NoError(t, err, "generate random ufixed, should be no error") + + narrowest, err := castBigIntToNearestPrimitive(randVal, uint16(bitSize)) + require.NoError(t, err, "cast random uint to nearest primitive failure") + + ufixedT, err := makeUfixedType(bitSize, precision) + require.NoError(t, err, "make ufixed type failure") + ufixedTstr := ufixedT.String() + (*pool)[Ufixed][ufixedIndex] = testUnit{serializedType: ufixedTstr, value: narrowest} + ufixedIndex++ + } + } + categorySelfRoundTripTest(t, (*pool)[Uint]) + categorySelfRoundTripTest(t, (*pool)[Ufixed]) + + (*pool)[Byte] = make([]testUnit, byteTestCaseCount) + for i := 0; i < byteTestCaseCount; i++ { + (*pool)[Byte][i] = testUnit{serializedType: byteType.String(), value: byte(i)} + } + categorySelfRoundTripTest(t, (*pool)[Byte]) + + (*pool)[Bool] = make([]testUnit, boolTestCaseCount) + (*pool)[Bool][0] = testUnit{serializedType: boolType.String(), value: false} + (*pool)[Bool][1] = testUnit{serializedType: boolType.String(), value: true} + categorySelfRoundTripTest(t, (*pool)[Bool]) + + maxAddress := new(big.Int).Lsh(big.NewInt(1), addressByteSize<<3) + (*pool)[Address] = make([]testUnit, addressTestCaseCount) + for i := 0; i < addressTestCaseCount; i++ { + randAddrVal, err := rand.Int(rand.Reader, maxAddress) + require.NoError(t, err, "generate random value for address, should be no error") + addrBytes := make([]byte, addressByteSize) + randAddrVal.FillBytes(addrBytes) + (*pool)[Address][i] = testUnit{serializedType: addressType.String(), value: addrBytes} + } + categorySelfRoundTripTest(t, (*pool)[Address]) + + (*pool)[String] = make([]testUnit, stringTestCaseCount*stringTestCaseSpecLenCount) + stringIndex := 0 + for length := 1; length <= stringTestCaseCount; length++ { + for i := 0; i < stringTestCaseSpecLenCount; i++ { + (*pool)[String][stringIndex] = testUnit{ + serializedType: stringType.String(), + value: gobberish.GenerateString(length), + } + stringIndex++ + } + } + categorySelfRoundTripTest(t, (*pool)[String]) +} + +func takeSomeFromCategoryAndGenerateArray( + t *testing.T, abiT BaseType, srtIndex int, takeNum uint16, pool *map[BaseType][]testUnit) { + + tempArray := make([]interface{}, takeNum) + for i := 0; i < int(takeNum); i++ { + index := srtIndex + i + if index >= len((*pool)[abiT]) { + index = srtIndex + } + tempArray[i] = (*pool)[abiT][index].value + } + tempT, err := TypeOf((*pool)[abiT][srtIndex].serializedType) + require.NoError(t, err, "type in test uint cannot be deserialized") + (*pool)[ArrayStatic] = append((*pool)[ArrayStatic], testUnit{ + serializedType: makeStaticArrayType(tempT, takeNum).String(), + value: tempArray, + }) + (*pool)[ArrayDynamic] = append((*pool)[ArrayDynamic], testUnit{ + serializedType: makeDynamicArrayType(tempT).String(), + value: tempArray, + }) +} + +func addArrayRandomValues(t *testing.T, pool *map[BaseType][]testUnit) { + for intIndex := 0; intIndex < len((*pool)[Uint]); intIndex += uintTestCaseCount { + takeSomeFromCategoryAndGenerateArray(t, Uint, intIndex, takeNum, pool) + } + takeSomeFromCategoryAndGenerateArray(t, Byte, 0, takeNum, pool) + takeSomeFromCategoryAndGenerateArray(t, Address, 0, takeNum, pool) + takeSomeFromCategoryAndGenerateArray(t, String, 0, takeNum, pool) + takeSomeFromCategoryAndGenerateArray(t, Bool, 0, takeNum, pool) + + categorySelfRoundTripTest(t, (*pool)[ArrayStatic]) + categorySelfRoundTripTest(t, (*pool)[ArrayDynamic]) +} + +func addTupleRandomValues(t *testing.T, slotRange BaseType, pool *map[BaseType][]testUnit) { + for i := 0; i < tupleTestCaseCount; i++ { + tupleLenBig, err := rand.Int(rand.Reader, big.NewInt(tupleMaxLength)) + require.NoError(t, err, "generate random tuple length should not return error") + tupleLen := tupleLenBig.Int64() + 1 + testUnits := make([]testUnit, tupleLen) + for index := 0; index < int(tupleLen); index++ { + tupleTypeIndexBig, err := rand.Int(rand.Reader, big.NewInt(int64(slotRange)+1)) + require.NoError(t, err, "generate random tuple element type index should not return error") + tupleTypeIndex := BaseType(tupleTypeIndexBig.Int64()) + tupleElemChoiceRange := len((*pool)[tupleTypeIndex]) + + tupleElemRangeIndexBig, err := rand.Int(rand.Reader, big.NewInt(int64(tupleElemChoiceRange))) + require.NoError(t, err, "generate random tuple element index in test pool should not return error") + tupleElemRangeIndex := tupleElemRangeIndexBig.Int64() + tupleElem := (*pool)[tupleTypeIndex][tupleElemRangeIndex] + testUnits[index] = tupleElem + } + elemValues := make([]interface{}, tupleLen) + elemTypes := make([]Type, tupleLen) + for index := 0; index < int(tupleLen); index++ { + elemValues[index] = testUnits[index].value + abiT, err := TypeOf(testUnits[index].serializedType) + require.NoError(t, err, "deserialize type failure for tuple elements") + elemTypes[index] = abiT + } + tupleT, err := MakeTupleType(elemTypes) + require.NoError(t, err, "make tuple type failure") + (*pool)[Tuple] = append((*pool)[Tuple], testUnit{ + serializedType: tupleT.String(), + value: elemValues, + }) + } +} + +func TestRandomABIEncodeDecodeRoundTrip(t *testing.T) { + testValuePool := make(map[BaseType][]testUnit) + addPrimitiveRandomValues(t, &testValuePool) + addArrayRandomValues(t, &testValuePool) + addTupleRandomValues(t, String, &testValuePool) + addTupleRandomValues(t, Tuple, &testValuePool) + categorySelfRoundTripTest(t, testValuePool[Tuple]) +} + +func TestParseArgJSONtoByteSlice(t *testing.T) { + makeRepeatSlice := func(size int, value string) []string { + slice := make([]string, size) + for i := range slice { + slice[i] = value + } + return slice + } + + tests := []struct { + argTypes []string + jsonArgs []string + expectedAppArgs [][]byte + }{ + { + argTypes: []string{}, + jsonArgs: []string{}, + expectedAppArgs: [][]byte{}, + }, + { + argTypes: []string{"uint8"}, + jsonArgs: []string{"100"}, + expectedAppArgs: [][]byte{{100}}, + }, + { + argTypes: []string{"uint8", "uint16"}, + jsonArgs: []string{"100", "65535"}, + expectedAppArgs: [][]byte{{100}, {255, 255}}, + }, + { + argTypes: makeRepeatSlice(15, "string"), + jsonArgs: []string{ + `"a"`, + `"b"`, + `"c"`, + `"d"`, + `"e"`, + `"f"`, + `"g"`, + `"h"`, + `"i"`, + `"j"`, + `"k"`, + `"l"`, + `"m"`, + `"n"`, + `"o"`, + }, + expectedAppArgs: [][]byte{ + {00, 01, 97}, + {00, 01, 98}, + {00, 01, 99}, + {00, 01, 100}, + {00, 01, 101}, + {00, 01, 102}, + {00, 01, 103}, + {00, 01, 104}, + {00, 01, 105}, + {00, 01, 106}, + {00, 01, 107}, + {00, 01, 108}, + {00, 01, 109}, + {00, 01, 110}, + {00, 01, 111}, + }, + }, + { + argTypes: makeRepeatSlice(16, "string"), + jsonArgs: []string{ + `"a"`, + `"b"`, + `"c"`, + `"d"`, + `"e"`, + `"f"`, + `"g"`, + `"h"`, + `"i"`, + `"j"`, + `"k"`, + `"l"`, + `"m"`, + `"n"`, + `"o"`, + `"p"`, + }, + expectedAppArgs: [][]byte{ + {00, 01, 97}, + {00, 01, 98}, + {00, 01, 99}, + {00, 01, 100}, + {00, 01, 101}, + {00, 01, 102}, + {00, 01, 103}, + {00, 01, 104}, + {00, 01, 105}, + {00, 01, 106}, + {00, 01, 107}, + {00, 01, 108}, + {00, 01, 109}, + {00, 01, 110}, + {00, 04, 00, 07, 00, 01, 111, 00, 01, 112}, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("index=%d", i), func(t *testing.T) { + applicationArgs := [][]byte{} + err := ParseArgJSONtoByteSlice(test.argTypes, test.jsonArgs, &applicationArgs) + require.NoError(t, err) + require.Equal(t, test.expectedAppArgs, applicationArgs) + }) + } +} + +func TestParseMethodSignature(t *testing.T) { + tests := []struct { + signature string + name string + argTypes []string + returnType string + }{ + { + signature: "add(uint8,uint16,pay,account,txn)uint32", + name: "add", + argTypes: []string{"uint8", "uint16", "pay", "account", "txn"}, + returnType: "uint32", + }, + { + signature: "nothing()void", + name: "nothing", + argTypes: []string{}, + returnType: "void", + }, + { + signature: "tupleArgs((uint8,uint128),account,(string,(bool,bool)))bool", + name: "tupleArgs", + argTypes: []string{"(uint8,uint128)", "account", "(string,(bool,bool))"}, + returnType: "bool", + }, + { + signature: "tupleReturn(uint64)(bool,bool,bool)", + name: "tupleReturn", + argTypes: []string{"uint64"}, + returnType: "(bool,bool,bool)", + }, + { + signature: "tupleArgsAndReturn((uint8,uint128),account,(string,(bool,bool)))(bool,bool,bool)", + name: "tupleArgsAndReturn", + argTypes: []string{"(uint8,uint128)", "account", "(string,(bool,bool))"}, + returnType: "(bool,bool,bool)", + }, + } + + for _, test := range tests { + t.Run(test.signature, func(t *testing.T) { + name, argTypes, returnType, err := ParseMethodSignature(test.signature) + require.NoError(t, err) + require.Equal(t, test.name, name) + require.Equal(t, test.argTypes, argTypes) + require.Equal(t, test.returnType, returnType) + }) + } +} + +func TestInferToSlice(t *testing.T) { + var emptySlice []int + tests := []struct { + toBeInferred interface{} + length int + }{ + { + toBeInferred: []int{}, + length: 0, + }, + { + toBeInferred: make([]int, 0), + length: 0, + }, + { + toBeInferred: emptySlice, + length: 0, + }, + { + toBeInferred: [0]int{}, + length: 0, + }, + { + toBeInferred: [32]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}, + length: 32, + }, + { + toBeInferred: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}, + length: 32, + }, + } + + for i, test := range tests { + inferredSlice, err := inferToSlice(test.toBeInferred) + require.NoError(t, err, "inferToSlice on testcase %d failed to successfully infer %v", i, test.toBeInferred) + require.Equal(t, test.length, len(inferredSlice), "inferToSlice on testcase %d inferred different length, expected %d", i, test.length) + } + + // one more testcase for totally nil (with no type information) is bad, should not pass the test + _, err := inferToSlice(nil) + require.EqualError( + t, err, + "cannot infer an interface value as a slice of interface element", + "inferToSlice should return type inference error when passed in nil with unexpected Kind") + + // one moar testcase for wrong typed nil is bad, should not pass the test + var nilPt *uint64 = nil + _, err = inferToSlice(nilPt) + require.EqualError( + t, err, + "cannot infer an interface value as a slice of interface element", + "inferToSlice should return type inference error when passing argument type other than slice or array") +} diff --git a/avm-abi/json.go b/avm-abi/json.go new file mode 100644 index 0000000..73cf0bc --- /dev/null +++ b/avm-abi/json.go @@ -0,0 +1,291 @@ +package abi + +import ( + "bytes" + "crypto/sha512" + "encoding/base32" + "encoding/json" + "fmt" + "math/big" +) + +// NOTE: discussion about go-algorand-sdk +// https://github.com/algorand/go-algorand/pull/3375#issuecomment-1007536841 + +var base32Encoder = base32.StdEncoding.WithPadding(base32.NoPadding) + +func addressCheckSum(addressBytes [addressByteSize]byte) []byte { + hashed := sha512.Sum512_256(addressBytes[:]) + return hashed[addressByteSize-checksumByteSize:] +} + +func addressToString(addressBytes [addressByteSize]byte) string { + checksum := addressCheckSum(addressBytes) + + var addressBytesAndChecksum [addressByteSize + checksumByteSize]byte + copy(addressBytesAndChecksum[:], addressBytes[:]) + copy(addressBytesAndChecksum[addressByteSize:], checksum) + + return base32Encoder.EncodeToString(addressBytesAndChecksum[:]) +} + +func addressFromString(addressString string) ([addressByteSize]byte, error) { + decoded, err := base32Encoder.DecodeString(addressString) + if err != nil { + return [addressByteSize]byte{}, + fmt.Errorf("cannot cast encoded address string (%s) to address: base32 decode error: %w", addressString, err) + } + if len(decoded) != addressByteSize+checksumByteSize { + return [addressByteSize]byte{}, + fmt.Errorf( + "cannot cast encoded address string (%s) to address: "+ + "decoded byte length should equal %d with address and checksum", + addressString, addressByteSize+checksumByteSize, + ) + } + var addressBytes [addressByteSize]byte + copy(addressBytes[:], decoded[:]) + + checksum := addressCheckSum(addressBytes) + if !bytes.Equal(checksum, decoded[addressByteSize:]) { + return [addressByteSize]byte{}, fmt.Errorf( + "cannot cast encoded address string (%s) to address: decoded checksum mismatch, %v != %v", + addressString, checksum, decoded[addressByteSize:], + ) + } + + return addressBytes, nil +} + +func castBigIntToNearestPrimitive(num *big.Int, bitSize uint16) (interface{}, error) { + if num.BitLen() > int(bitSize) { + return nil, fmt.Errorf("cast big int to nearest primitive failure: %v >= 2^%d", num, bitSize) + } else if num.Sign() < 0 { + return nil, fmt.Errorf("cannot cast big int to near primitive: %v < 0", num) + } + + switch bitSize / 8 { + case 1: + return uint8(num.Uint64()), nil + case 2: + return uint16(num.Uint64()), nil + case 3, 4: + return uint32(num.Uint64()), nil + case 5, 6, 7, 8: + return num.Uint64(), nil + default: + return num, nil + } +} + +// MarshalToJSON convert golang value to JSON format from ABI type +func (t Type) MarshalToJSON(value interface{}) ([]byte, error) { + switch t.abiTypeID { + case Uint: + bytesUint, err := encodeInt(value, t.bitSize) + if err != nil { + return nil, err + } + return new(big.Int).SetBytes(bytesUint).MarshalJSON() + case Ufixed: + denom := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(t.precision)), nil) + encodedUint, err := encodeInt(value, t.bitSize) + if err != nil { + return nil, err + } + return []byte(new(big.Rat).SetFrac(new(big.Int).SetBytes(encodedUint), denom).FloatString(int(t.precision))), nil + case Bool: + boolValue, ok := value.(bool) + if !ok { + return nil, fmt.Errorf("cannot infer to bool for marshal to JSON") + } + return json.Marshal(boolValue) + case Byte: + byteValue, ok := value.(byte) + if !ok { + return nil, fmt.Errorf("cannot infer to byte for marshal to JSON") + } + return json.Marshal(byteValue) + case Address: + var addressBytes [addressByteSize]byte + switch valueCasted := value.(type) { + case []byte: + if len(valueCasted) != addressByteSize { + return nil, fmt.Errorf("address byte slice length not equal to 32 byte") + } + copy(addressBytes[:], valueCasted[:]) + case [addressByteSize]byte: + copy(addressBytes[:], valueCasted[:]) + default: + return nil, fmt.Errorf("cannot infer to byte slice/array for marshal to JSON") + } + return json.Marshal(addressToString(addressBytes)) + case ArrayStatic, ArrayDynamic: + values, err := inferToSlice(value) + if err != nil { + return nil, err + } + if t.abiTypeID == ArrayStatic && int(t.staticLength) != len(values) { + return nil, fmt.Errorf("length of slice %d != type specific length %d", len(values), t.staticLength) + } + if t.childTypes[0].abiTypeID == Byte { + byteArr := make([]byte, len(values)) + for i := 0; i < len(values); i++ { + tempByte, ok := values[i].(byte) + if !ok { + return nil, fmt.Errorf("cannot infer byte element from slice") + } + byteArr[i] = tempByte + } + return json.Marshal(byteArr) + } + rawMsgSlice := make([]json.RawMessage, len(values)) + for i := 0; i < len(values); i++ { + rawMsgSlice[i], err = t.childTypes[0].MarshalToJSON(values[i]) + if err != nil { + return nil, err + } + } + return json.Marshal(rawMsgSlice) + case String: + stringVal, ok := value.(string) + if !ok { + return nil, fmt.Errorf("cannot infer to string for marshal to JSON") + } + return json.Marshal(stringVal) + case Tuple: + values, err := inferToSlice(value) + if err != nil { + return nil, err + } + if len(values) != int(t.staticLength) { + return nil, fmt.Errorf("tuple element number != value slice length") + } + rawMsgSlice := make([]json.RawMessage, len(values)) + for i := 0; i < len(values); i++ { + rawMsgSlice[i], err = t.childTypes[i].MarshalToJSON(values[i]) + if err != nil { + return nil, err + } + } + return json.Marshal(rawMsgSlice) + default: + return nil, fmt.Errorf("cannot infer ABI type for marshalling value to JSON") + } +} + +// UnmarshalFromJSON convert bytes to golang value following ABI type and encoding rules +func (t Type) UnmarshalFromJSON(jsonEncoded []byte) (interface{}, error) { + switch t.abiTypeID { + case Uint: + num := new(big.Int) + if err := num.UnmarshalJSON(jsonEncoded); err != nil { + return nil, fmt.Errorf("cannot cast JSON encoded (%s) to uint: %w", string(jsonEncoded), err) + } + return castBigIntToNearestPrimitive(num, t.bitSize) + case Ufixed: + floatTemp := new(big.Rat) + if err := floatTemp.UnmarshalText(jsonEncoded); err != nil { + return nil, fmt.Errorf("cannot cast JSON encoded (%s) to ufixed: %w", string(jsonEncoded), err) + } + denom := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(t.precision)), nil) + denomRat := new(big.Rat).SetInt(denom) + numeratorRat := new(big.Rat).Mul(denomRat, floatTemp) + if !numeratorRat.IsInt() { + return nil, fmt.Errorf("cannot cast JSON encoded (%s) to ufixed: precision out of range", string(jsonEncoded)) + } + return castBigIntToNearestPrimitive(numeratorRat.Num(), t.bitSize) + case Bool: + var elem bool + if err := json.Unmarshal(jsonEncoded, &elem); err != nil { + return nil, fmt.Errorf("cannot cast JSON encoded (%s) to bool: %w", string(jsonEncoded), err) + } + return elem, nil + case Byte: + var elem byte + if err := json.Unmarshal(jsonEncoded, &elem); err != nil { + return nil, fmt.Errorf("cannot cast JSON encoded to byte: %w", err) + } + return elem, nil + case Address: + var addrStr string + if err := json.Unmarshal(jsonEncoded, &addrStr); err != nil { + return nil, fmt.Errorf("cannot cast JSON encoded (%s) to address string: %w", string(jsonEncoded), err) + } + + addrBytes, err := addressFromString(addrStr) + if err != nil { + return nil, err + } + + return addrBytes[:], nil + case ArrayStatic, ArrayDynamic: + if t.childTypes[0].abiTypeID == Byte && bytes.HasPrefix(jsonEncoded, []byte{'"'}) { + var byteArr []byte + err := json.Unmarshal(jsonEncoded, &byteArr) + if err != nil { + return nil, fmt.Errorf("cannot cast JSON encoded (%s) to bytes: %w", string(jsonEncoded), err) + } + if t.abiTypeID == ArrayStatic && len(byteArr) != int(t.staticLength) { + return nil, fmt.Errorf("length of slice %d != type specific length %d", len(byteArr), t.staticLength) + } + outInterface := make([]interface{}, len(byteArr)) + for i := 0; i < len(byteArr); i++ { + outInterface[i] = byteArr[i] + } + return outInterface, nil + } + var elems []json.RawMessage + if err := json.Unmarshal(jsonEncoded, &elems); err != nil { + return nil, fmt.Errorf("cannot cast JSON encoded (%s) to array: %w", string(jsonEncoded), err) + } + if t.abiTypeID == ArrayStatic && len(elems) != int(t.staticLength) { + return nil, fmt.Errorf("JSON array element number != ABI array elem number") + } + values := make([]interface{}, len(elems)) + for i := 0; i < len(elems); i++ { + tempValue, err := t.childTypes[0].UnmarshalFromJSON(elems[i]) + if err != nil { + return nil, err + } + values[i] = tempValue + } + return values, nil + case String: + stringEncoded := string(jsonEncoded) + if bytes.HasPrefix(jsonEncoded, []byte{'"'}) { + var stringVar string + if err := json.Unmarshal(jsonEncoded, &stringVar); err != nil { + return nil, fmt.Errorf("cannot cast JSON encoded (%s) to string: %w", stringEncoded, err) + } + return stringVar, nil + } else if bytes.HasPrefix(jsonEncoded, []byte{'['}) { + var elems []byte + if err := json.Unmarshal(jsonEncoded, &elems); err != nil { + return nil, fmt.Errorf("cannot cast JSON encoded (%s) to string: %w", stringEncoded, err) + } + return string(elems), nil + } else { + return nil, fmt.Errorf("cannot cast JSON encoded (%s) to string", stringEncoded) + } + case Tuple: + var elems []json.RawMessage + if err := json.Unmarshal(jsonEncoded, &elems); err != nil { + return nil, fmt.Errorf("cannot cast JSON encoded (%s) to array for tuple: %w", string(jsonEncoded), err) + } + if len(elems) != int(t.staticLength) { + return nil, fmt.Errorf("JSON array element number != ABI tuple elem number") + } + values := make([]interface{}, len(elems)) + for i := 0; i < len(elems); i++ { + tempValue, err := t.childTypes[i].UnmarshalFromJSON(elems[i]) + if err != nil { + return nil, err + } + values[i] = tempValue + } + return values, nil + default: + return nil, fmt.Errorf("cannot cast JSON encoded %s to ABI encoding stuff", string(jsonEncoded)) + } +} diff --git a/avm-abi/json_test.go b/avm-abi/json_test.go new file mode 100644 index 0000000..ab489e9 --- /dev/null +++ b/avm-abi/json_test.go @@ -0,0 +1,320 @@ +package abi + +import ( + "fmt" + "math/big" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAddress(t *testing.T) { + t.Parallel() + + t.Run("valid", func(t *testing.T) { + t.Parallel() + testCases := []struct { + addressString string + addressBytes [32]byte + addressChecksum [4]byte + }{ + { + addressString: "CAFFDSU6TYXNDC6V6R5XAOHBWBD4MH36TNUWCW4D6HKV7EKHP33Q74JAFM", + addressBytes: [32]byte{16, 10, 81, 202, 158, 158, 46, 209, 139, 213, 244, 123, 112, 56, 225, 176, 71, 198, 31, 126, 155, 105, 97, 91, 131, 241, 213, 95, 145, 71, 126, 247}, + addressChecksum: [4]byte{15, 241, 32, 43}, + }, + { + addressString: "OXV2VEY7QJUXGOHEVFSL7LTBMOTYI4VORBJ37CGCHKBPJSH6IZQMHDPFRA", + addressBytes: [32]byte{117, 235, 170, 147, 31, 130, 105, 115, 56, 228, 169, 100, 191, 174, 97, 99, 167, 132, 114, 174, 136, 83, 191, 136, 194, 58, 130, 244, 200, 254, 70, 96}, + addressChecksum: [4]byte{195, 141, 229, 136}, + }, + { + addressString: "BFLADE6DETJQU7DABJANLCKH3PEFTWQQGZ34YHRPRKWYNEYC3DRWKJWZZA", + addressBytes: [32]byte{9, 86, 1, 147, 195, 36, 211, 10, 124, 96, 10, 64, 213, 137, 71, 219, 200, 89, 218, 16, 54, 119, 204, 30, 47, 138, 173, 134, 147, 2, 216, 227}, + addressChecksum: [4]byte{101, 38, 217, 200}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.addressString, func(t *testing.T) { + require.Equal(t, testCase.addressChecksum[:], addressCheckSum(testCase.addressBytes)) + require.Equal(t, testCase.addressString, addressToString(testCase.addressBytes)) + + actualBytes, err := addressFromString(testCase.addressString) + require.NoError(t, err) + require.Equal(t, testCase.addressBytes, actualBytes) + }) + } + }) + + t.Run("invalid", func(t *testing.T) { + t.Parallel() + testCases := []struct { + addressString string + expectedError string + }{ + { + // incorrect checksum + addressString: "CAFFDSU6TYXNDC6V6R5XAOHBWBD4MH36TNUWCW4D6HKV7EKHP33Q74JAQM", + expectedError: "decoded checksum mismatch", + }, + { + // too many bytes + addressString: "CAFFDSU6TYXNDC6V6R5XAOHBWBD4MH36TNUWCW4D6HKV7EKHP33Q74JAFMFM", + expectedError: "decoded byte length should equal 36 with address and checksum", + }, + { + // too few bytes + addressString: "CAFFDSU6TYXNDC6V6R5XAOHBWBD4MH36TNUWCW4D6HKV7EKHP33Q74JA", + expectedError: "decoded byte length should equal 36 with address and checksum", + }, + { + // not base32 + addressString: "!!!", + expectedError: "base32 decode error", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.addressString, func(t *testing.T) { + _, err := addressFromString(testCase.addressString) + require.ErrorContains(t, err, testCase.expectedError) + }) + } + }) +} + +func TestUnmarshalFromJSON(t *testing.T) { + t.Parallel() + var testCases = []struct { + input string + typeStr string + expected interface{} + }{ + { + input: `[true, [0, 1, 2], 17]`, + typeStr: `(bool,byte[],uint64)`, + expected: []interface{}{ + true, + []interface{}{byte(0), byte(1), byte(2)}, + uint64(17), + }, + }, + { + input: `[true, "AAEC", 17]`, + typeStr: `(bool,byte[],uint64)`, + expected: []interface{}{ + true, + []interface{}{byte(0), byte(1), byte(2)}, + uint64(17), + }, + }, + { + input: `"AQEEBQEE"`, + typeStr: `byte[6]`, + expected: []interface{}{byte(1), byte(1), byte(4), byte(5), byte(1), byte(4)}, + }, + { + input: `[[0, [true, false], "utf-8"], [18446744073709551615, [false, true], "pistachio"]]`, + typeStr: `(uint64,bool[2],string)[]`, + expected: []interface{}{ + []interface{}{uint64(0), []interface{}{true, false}, "utf-8"}, + []interface{}{^uint64(0), []interface{}{false, true}, "pistachio"}, + }, + }, + { + input: `[]`, + typeStr: `(uint64,bool[2],string)[]`, + expected: []interface{}{}, + }, + { + input: "[]", + typeStr: "()", + expected: []interface{}{}, + }, + { + input: "[65, 66, 67]", + typeStr: "string", + expected: "ABC", + }, + { + input: "[]", + typeStr: "string", + expected: "", + }, + { + input: "123.456", + typeStr: "ufixed64x3", + expected: uint64(123456), + }, + { + input: `"optin"`, + typeStr: "string", + expected: "optin", + }, + { + input: `"AAEC"`, + typeStr: "byte[3]", + expected: []interface{}{byte(0), byte(1), byte(2)}, + }, + { + input: `["uwu",["AAEC",12.34]]`, + typeStr: "(string,(byte[3],ufixed64x3))", + expected: []interface{}{"uwu", []interface{}{[]interface{}{byte(0), byte(1), byte(2)}, uint64(12340)}}, + }, + { + input: `[399,"should pass",[true,false,false,true]]`, + typeStr: "(uint64,string,bool[])", + expected: []interface{}{uint64(399), "should pass", []interface{}{true, false, false, true}}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.input, func(t *testing.T) { + abiT, err := TypeOf(testCase.typeStr) + require.NoError(t, err, "fail to construct ABI type: %s", testCase.typeStr) + + res, err := abiT.UnmarshalFromJSON([]byte(testCase.input)) + require.NoError(t, err, "fail to unmarshal JSON to interface: (%s): %v", testCase.input, err) + require.Equal(t, testCase.expected, res, "%v not matching with expected value %v", res, testCase.expected) + + resEncoded, err := abiT.Encode(res) + require.NoError(t, err, "fail to encode %v to ABI bytes: %v", res, err) + resDecoded, err := abiT.Decode(resEncoded) + require.NoError(t, err, "fail to decode ABI bytes of %v: %v", res, err) + require.Equal(t, res, resDecoded, "ABI encode-decode round trip: %v not match with expected %v", resDecoded, res) + }) + } +} + +func TestMarshalToJSON(t *testing.T) { + t.Parallel() + var testCases = []struct { + input interface{} + typeStr string + expected string + }{ + { + input: true, + typeStr: `bool`, + expected: `true`, + }, + { + input: false, + typeStr: `bool`, + expected: `false`, + }, + { + input: uint64(117), + typeStr: `uint64`, + expected: `117`, + }, + { + input: big.NewInt(5834), + typeStr: `uint128`, + expected: `5834`, + }, + { + input: []interface{}{uint8(0), big.NewInt(1), uint32(2)}, + typeStr: `(uint8,uint64,uint32)`, + expected: `[0,1,2]`, + }, + { + input: [3]interface{}{uint8(0), big.NewInt(1), uint32(2)}, + typeStr: `(uint8,uint64,uint32)`, + expected: `[0,1,2]`, + }, + { + input: []uint8{0, 1, 2}, + typeStr: `(uint8,uint64,uint32)`, + expected: `[0,1,2]`, + }, + { + input: [3]uint8{0, 1, 2}, + typeStr: `(uint8,uint64,uint32)`, + expected: `[0,1,2]`, + }, + { + input: []uint64{0, 1, 2}, + typeStr: `(uint8,uint64,uint32)`, + expected: `[0,1,2]`, + }, + { + input: [3]uint64{0, 1, 2}, + typeStr: `(uint8,uint64,uint32)`, + expected: `[0,1,2]`, + }, + { + input: []interface{}{uint8(0), big.NewInt(1), uint32(2)}, + typeStr: `uint8[]`, + expected: `[0,1,2]`, + }, + { + input: [3]interface{}{uint8(0), big.NewInt(1), uint32(2)}, + typeStr: `uint8[]`, + expected: `[0,1,2]`, + }, + { + input: []uint8{0, 1, 2}, + typeStr: `uint8[]`, + expected: `[0,1,2]`, + }, + { + input: [3]uint8{0, 1, 2}, + typeStr: `uint8[]`, + expected: `[0,1,2]`, + }, + { + input: []uint64{0, 1, 2}, + typeStr: `uint8[]`, + expected: `[0,1,2]`, + }, + { + input: [3]uint64{0, 1, 2}, + typeStr: `uint8[]`, + expected: `[0,1,2]`, + }, + { + input: []interface{}{byte(0), byte(1), byte(2)}, + typeStr: `byte[]`, + expected: `"AAEC"`, + }, + { + input: [3]interface{}{byte(0), byte(1), byte(2)}, + typeStr: `byte[]`, + expected: `"AAEC"`, + }, + { + input: []byte{0, 1, 2}, + typeStr: `byte[]`, + expected: `"AAEC"`, + }, + { + input: [3]byte{0, 1, 2}, + typeStr: `byte[]`, + expected: `"AAEC"`, + }, + { + input: []byte{16, 10, 81, 202, 158, 158, 46, 209, 139, 213, 244, 123, 112, 56, 225, 176, 71, 198, 31, 126, 155, 105, 97, 91, 131, 241, 213, 95, 145, 71, 126, 247}, + typeStr: `address`, + expected: `"CAFFDSU6TYXNDC6V6R5XAOHBWBD4MH36TNUWCW4D6HKV7EKHP33Q74JAFM"`, + }, + { + input: [32]byte{16, 10, 81, 202, 158, 158, 46, 209, 139, 213, 244, 123, 112, 56, 225, 176, 71, 198, 31, 126, 155, 105, 97, 91, 131, 241, 213, 95, 145, 71, 126, 247}, + typeStr: `address`, + expected: `"CAFFDSU6TYXNDC6V6R5XAOHBWBD4MH36TNUWCW4D6HKV7EKHP33Q74JAFM"`, + }, + } + + for i, testCase := range testCases { + t.Run(fmt.Sprintf("i=%d", i), func(t *testing.T) { + abiT, err := TypeOf(testCase.typeStr) + require.NoError(t, err, "fail to construct ABI type: %s", testCase.typeStr) + + actualJSON, err := abiT.MarshalToJSON(testCase.input) + require.NoError(t, err) + + require.Equal(t, testCase.expected, string(actualJSON)) + }) + } +} diff --git a/avm-abi/type.go b/avm-abi/type.go new file mode 100644 index 0000000..474d7e4 --- /dev/null +++ b/avm-abi/type.go @@ -0,0 +1,482 @@ +package abi + +import ( + "fmt" + "math" + "regexp" + "strconv" + "strings" +) + +/* + ABI-Types: uint: An N-bit unsigned integer (8 <= N <= 512 and N % 8 = 0). + | byte (alias for uint8) + | ufixed x (8 <= N <= 512, N % 8 = 0, and 0 < M <= 160) + | bool + | address (alias for byte[32]) + | [] + | [] + | string + | (T1, ..., Tn) +*/ + +// BaseType is an type-alias for uint32. A BaseType value indicates the type of an ABI value. +type BaseType uint32 + +const ( + // Uint is the index (0) for `Uint` type in ABI encoding. + Uint BaseType = iota + // Byte is the index (1) for `Byte` type in ABI encoding. + Byte + // Ufixed is the index (2) for `UFixed` type in ABI encoding. + Ufixed + // Bool is the index (3) for `Bool` type in ABI encoding. + Bool + // ArrayStatic is the index (4) for static length array ([length]) type in ABI encoding. + ArrayStatic + // Address is the index (5) for `Address` type in ABI encoding (an type alias of Byte[32]). + Address + // ArrayDynamic is the index (6) for dynamic length array ([]) type in ABI encoding. + ArrayDynamic + // String is the index (7) for `String` type in ABI encoding (an type alias of Byte[]). + String + // Tuple is the index (8) for tuple `(, ..., )` in ABI encoding. + Tuple +) + +const ( + addressByteSize = 32 + checksumByteSize = 4 + singleByteSize = 1 + singleBoolSize = 1 + lengthEncodeByteSize = 2 + abiEncodingLengthLimit = 1 << 16 +) + +// Type is the struct that stores information about an ABI value's type. +type Type struct { + abiTypeID BaseType + childTypes []Type + + // only can be applied to `uint` bitSize or `ufixed` bitSize + bitSize uint16 + // only can be applied to `ufixed` precision + precision uint16 + + // length for static array / tuple + /* + by ABI spec, len over binary array returns number of bytes + the type is uint16, which allows for only length in [0, 2^16 - 1] + representation of static length can only be constrained in uint16 type + */ + // NOTE may want to change back to uint32/uint64 + staticLength uint16 +} + +// String serialize an ABI Type to a string in ABI encoding. +func (t Type) String() string { + switch t.abiTypeID { + case Uint: + return fmt.Sprintf("uint%d", t.bitSize) + case Byte: + return "byte" + case Ufixed: + return fmt.Sprintf("ufixed%dx%d", t.bitSize, t.precision) + case Bool: + return "bool" + case ArrayStatic: + return fmt.Sprintf("%s[%d]", t.childTypes[0].String(), t.staticLength) + case Address: + return "address" + case ArrayDynamic: + return t.childTypes[0].String() + "[]" + case String: + return "string" + case Tuple: + typeStrings := make([]string, len(t.childTypes)) + for i := 0; i < len(t.childTypes); i++ { + typeStrings[i] = t.childTypes[i].String() + } + return "(" + strings.Join(typeStrings, ",") + ")" + default: + panic("Type Serialization Error, fail to infer from abiTypeID (bruh you shouldn't be here)") + } +} + +var staticArrayRegexp = regexp.MustCompile(`^([a-z\d\[\](),]+)\[([1-9][\d]*)]$`) +var ufixedRegexp = regexp.MustCompile(`^ufixed([1-9][\d]*)x([1-9][\d]*)$`) + +// TypeOf parses an ABI type string. +// For example: `TypeOf("(uint64,byte[])")` +func TypeOf(str string) (Type, error) { + switch { + case strings.HasSuffix(str, "[]"): + arrayArgType, err := TypeOf(str[:len(str)-2]) + if err != nil { + return Type{}, err + } + return makeDynamicArrayType(arrayArgType), nil + case strings.HasSuffix(str, "]"): + stringMatches := staticArrayRegexp.FindStringSubmatch(str) + // match the string itself, array element type, then array length + if len(stringMatches) != 3 { + return Type{}, fmt.Errorf(`static array ill formated: "%s"`, str) + } + // guaranteed that the length of array is existing + arrayLengthStr := stringMatches[2] + // allowing only decimal static array length, with limit size to 2^16 - 1 + arrayLength, err := strconv.ParseUint(arrayLengthStr, 10, 16) + if err != nil { + return Type{}, err + } + // parse the array element type + arrayType, err := TypeOf(stringMatches[1]) + if err != nil { + return Type{}, err + } + return makeStaticArrayType(arrayType, uint16(arrayLength)), nil + case strings.HasPrefix(str, "uint"): + typeSize, err := strconv.ParseUint(str[4:], 10, 16) + if err != nil { + return Type{}, fmt.Errorf(`ill formed uint type: "%s"`, str) + } + return makeUintType(int(typeSize)) + case str == "byte": + return byteType, nil + case strings.HasPrefix(str, "ufixed"): + stringMatches := ufixedRegexp.FindStringSubmatch(str) + // match string itself, then type-bitSize, and type-precision + if len(stringMatches) != 3 { + return Type{}, fmt.Errorf(`ill formed ufixed type: "%s"`, str) + } + // guaranteed that there are 2 uint strings in ufixed string + ufixedSize, err := strconv.ParseUint(stringMatches[1], 10, 16) + if err != nil { + return Type{}, err + } + ufixedPrecision, err := strconv.ParseUint(stringMatches[2], 10, 16) + if err != nil { + return Type{}, err + } + return makeUfixedType(int(ufixedSize), int(ufixedPrecision)) + case str == "bool": + return boolType, nil + case str == "address": + return addressType, nil + case str == "string": + return stringType, nil + case len(str) >= 2 && str[0] == '(' && str[len(str)-1] == ')': + tupleContent, err := parseTupleContent(str[1 : len(str)-1]) + if err != nil { + return Type{}, err + } + tupleTypes := make([]Type, len(tupleContent)) + for i := 0; i < len(tupleContent); i++ { + ti, err := TypeOf(tupleContent[i]) + if err != nil { + return Type{}, err + } + tupleTypes[i] = ti + } + return MakeTupleType(tupleTypes) + default: + return Type{}, fmt.Errorf(`cannot convert the string "%s" to an ABI type`, str) + } +} + +// segment keeps track of the start and end of a segment in a string. +type segment struct{ left, right int } + +// parseTupleContent splits an ABI encoded string for tuple type into multiple sub-strings. +// Each sub-string represents a content type of the tuple type. +// The argument str is the content between parentheses of tuple, i.e. +// (...... str ......) +// ^ ^ +func parseTupleContent(str string) ([]string, error) { + // if the tuple type content is empty (which is also allowed) + // just return the empty string list + if len(str) == 0 { + return []string{}, nil + } + + // the following 2 checks want to make sure input string can be separated by comma + // with form: "...substr_0,...substr_1,...,...substr_k" + + // str should noe have leading/tailing comma + if strings.HasSuffix(str, ",") || strings.HasPrefix(str, ",") { + return []string{}, fmt.Errorf("parsing error: tuple content should not start with comma") + } + + // str should not have consecutive commas contained + if strings.Contains(str, ",,") { + return []string{}, fmt.Errorf("no consecutive commas") + } + + var parenSegmentRecord = make([]segment, 0) + var stack []int + + // get the most exterior parentheses segment (not overlapped by other parentheses) + // illustration: "*****,(*****),*****" => ["*****", "(*****)", "*****"] + // once iterate to left paren (, stack up by 1 in stack + // iterate to right paren ), pop 1 in stack + // if iterate to right paren ) with stack height 0, find a parenthesis segment "(******)" + for index, chr := range str { + if chr == '(' { + stack = append(stack, index) + } else if chr == ')' { + if len(stack) == 0 { + return []string{}, fmt.Errorf("unpaired parentheses: %s", str) + } + leftParenIndex := stack[len(stack)-1] + stack = stack[:len(stack)-1] + if len(stack) == 0 { + parenSegmentRecord = append(parenSegmentRecord, segment{ + left: leftParenIndex, + right: index, + }) + } + } + } + if len(stack) != 0 { + return []string{}, fmt.Errorf("unpaired parentheses: %s", str) + } + + // take out tuple-formed type str in tuple argument + strCopied := str + for i := len(parenSegmentRecord) - 1; i >= 0; i-- { + parenSeg := parenSegmentRecord[i] + strCopied = strCopied[:parenSeg.left] + strCopied[parenSeg.right+1:] + } + + // split the string without parenthesis segments + tupleStrSegs := strings.Split(strCopied, ",") + + // the empty strings are placeholders for parenthesis segments + // put the parenthesis segments back into segment list + parenSegCount := 0 + for index, segStr := range tupleStrSegs { + if segStr == "" { + parenSeg := parenSegmentRecord[parenSegCount] + tupleStrSegs[index] = str[parenSeg.left : parenSeg.right+1] + parenSegCount++ + } + } + + return tupleStrSegs, nil +} + +// makeUintType makes `Uint` ABI type by taking a type bitSize argument. +// The range of type bitSize is [8, 512] and type bitSize % 8 == 0. +func makeUintType(typeSize int) (Type, error) { + if typeSize%8 != 0 || typeSize < 8 || typeSize > 512 { + return Type{}, fmt.Errorf("unsupported uint type bitSize: %d", typeSize) + } + return Type{ + abiTypeID: Uint, + bitSize: uint16(typeSize), + }, nil +} + +var ( + // byteType is ABI type constant for byte + byteType = Type{abiTypeID: Byte} + + // boolType is ABI type constant for bool + boolType = Type{abiTypeID: Bool} + + // addressType is ABI type constant for address + addressType = Type{abiTypeID: Address} + + // stringType is ABI type constant for string + stringType = Type{abiTypeID: String} +) + +// makeUfixedType makes `UFixed` ABI type by taking type bitSize and type precision as arguments. +// The range of type bitSize is [8, 512] and type bitSize % 8 == 0. +// The range of type precision is [1, 160]. +func makeUfixedType(typeSize int, typePrecision int) (Type, error) { + if typeSize%8 != 0 || typeSize < 8 || typeSize > 512 { + return Type{}, fmt.Errorf("unsupported ufixed type bitSize: %d", typeSize) + } + if typePrecision > 160 || typePrecision < 1 { + return Type{}, fmt.Errorf("unsupported ufixed type precision: %d", typePrecision) + } + return Type{ + abiTypeID: Ufixed, + bitSize: uint16(typeSize), + precision: uint16(typePrecision), + }, nil +} + +// makeStaticArrayType makes static length array ABI type by taking +// array element type and array length as arguments. +func makeStaticArrayType(argumentType Type, arrayLength uint16) Type { + return Type{ + abiTypeID: ArrayStatic, + childTypes: []Type{argumentType}, + staticLength: arrayLength, + } +} + +// makeDynamicArrayType makes dynamic length array by taking array element type as argument. +func makeDynamicArrayType(argumentType Type) Type { + return Type{ + abiTypeID: ArrayDynamic, + childTypes: []Type{argumentType}, + } +} + +// MakeTupleType makes tuple ABI type by taking an array of tuple element types as argument. +func MakeTupleType(argumentTypes []Type) (Type, error) { + if len(argumentTypes) >= math.MaxUint16 { + return Type{}, fmt.Errorf("tuple type child type number larger than maximum uint16 error") + } + return Type{ + abiTypeID: Tuple, + childTypes: argumentTypes, + staticLength: uint16(len(argumentTypes)), + }, nil +} + +// Equal method decides the equality of two types: t == t0. +func (t Type) Equal(t0 Type) bool { + if t.abiTypeID != t0.abiTypeID { + return false + } + if t.precision != t0.precision || t.bitSize != t0.bitSize { + return false + } + if t.staticLength != t0.staticLength { + return false + } + if len(t.childTypes) != len(t0.childTypes) { + return false + } + for i := 0; i < len(t.childTypes); i++ { + if !t.childTypes[i].Equal(t0.childTypes[i]) { + return false + } + } + + return true +} + +// IsDynamic method decides if an ABI type is dynamic or static. +func (t Type) IsDynamic() bool { + switch t.abiTypeID { + case ArrayDynamic, String: + return true + default: + for _, childT := range t.childTypes { + if childT.IsDynamic() { + return true + } + } + return false + } +} + +// Assume that the current index on the list of type is an ABI bool type. +// It returns the difference between the current index and the index of the furthest consecutive Bool type. +func findBoolLR(typeList []Type, index int, delta int) int { + until := 0 + for { + curr := index + delta*until + if typeList[curr].abiTypeID == Bool { + if curr != len(typeList)-1 && delta > 0 { + until++ + } else if curr > 0 && delta < 0 { + until++ + } else { + break + } + } else { + until-- + break + } + } + return until +} + +// ByteLen method calculates the byte length of a static ABI type. +func (t Type) ByteLen() (int, error) { + switch t.abiTypeID { + case Address: + return addressByteSize, nil + case Byte: + return singleByteSize, nil + case Uint, Ufixed: + return int(t.bitSize / 8), nil + case Bool: + return singleBoolSize, nil + case ArrayStatic: + if t.childTypes[0].abiTypeID == Bool { + byteLen := int(t.staticLength+7) / 8 + return byteLen, nil + } + elemByteLen, err := t.childTypes[0].ByteLen() + if err != nil { + return -1, err + } + return int(t.staticLength) * elemByteLen, nil + case Tuple: + size := 0 + for i := 0; i < len(t.childTypes); i++ { + if t.childTypes[i].abiTypeID == Bool { + // search after bool + after := findBoolLR(t.childTypes, i, 1) + // shift the index + i += after + // get number of bool + boolNum := after + 1 + size += (boolNum + 7) / 8 + } else { + childByteSize, err := t.childTypes[i].ByteLen() + if err != nil { + return -1, err + } + size += childByteSize + } + } + return size, nil + default: + return -1, fmt.Errorf("%s is a dynamic type", t.String()) + } +} + +// AnyTransactionType is the ABI argument type string for a nonspecific transaction argument +const AnyTransactionType = "txn" + +// IsTransactionType checks if a type string represents a transaction type +// argument, such as "txn", "pay", "keyreg", etc. +func IsTransactionType(s string) bool { + switch s { + case AnyTransactionType, "pay", "keyreg", "acfg", "axfer", "afrz", "appl": + return true + default: + return false + } +} + +// AccountReferenceType is the ABI argument type string for account references +const AccountReferenceType = "account" + +// AssetReferenceType is the ABI argument type string for asset references +const AssetReferenceType = "asset" + +// ApplicationReferenceType is the ABI argument type string for application references +const ApplicationReferenceType = "application" + +// IsReferenceType checks if a type string represents a reference type argument, +// such as "account", "asset", or "application". +func IsReferenceType(s string) bool { + switch s { + case AccountReferenceType, AssetReferenceType, ApplicationReferenceType: + return true + default: + return false + } +} + +// VoidReturnType is the ABI return type string for a method that does not return any value +const VoidReturnType = "void" diff --git a/avm-abi/type_test.go b/avm-abi/type_test.go new file mode 100644 index 0000000..bfce2b2 --- /dev/null +++ b/avm-abi/type_test.go @@ -0,0 +1,591 @@ +package abi + +import ( + "fmt" + "math/rand" + "strconv" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestMakeTypeValid(t *testing.T) { + // uint + for i := 8; i <= 512; i += 8 { + uintType, err := makeUintType(i) + require.NoError(t, err, "make uint type in valid space should not return error") + expected := "uint" + strconv.Itoa(i) + actual := uintType.String() + require.Equal(t, expected, actual, "makeUintType: expected %s, actual %s", expected, actual) + } + // ufixed + for i := 8; i <= 512; i += 8 { + for j := 1; j <= 160; j++ { + ufixedType, err := makeUfixedType(i, j) + require.NoError(t, err, "make ufixed type in valid space should not return error") + expected := "ufixed" + strconv.Itoa(i) + "x" + strconv.Itoa(j) + actual := ufixedType.String() + require.Equal(t, expected, actual, + "TypeOf ufixed error: expected %s, actual %s", expected, actual) + } + } + // bool/strings/address/byte + dynamic/static array + tuple + var testcases = []struct { + input Type + testType string + expected string + }{ + {input: boolType, testType: "bool", expected: "bool"}, + {input: stringType, testType: "string", expected: "string"}, + {input: addressType, testType: "address", expected: "address"}, + {input: byteType, testType: "byte", expected: "byte"}, + // dynamic array + { + input: makeDynamicArrayType( + Type{ + abiTypeID: Uint, + bitSize: uint16(32), + }, + ), + testType: "dynamic array", + expected: "uint32[]", + }, + { + input: makeDynamicArrayType( + makeDynamicArrayType( + byteType, + ), + ), + testType: "dynamic array", + expected: "byte[][]", + }, + { + input: makeStaticArrayType( + Type{ + abiTypeID: Ufixed, + bitSize: uint16(128), + precision: uint16(10), + }, + uint16(100), + ), + testType: "static array", + expected: "ufixed128x10[100]", + }, + { + input: makeStaticArrayType( + makeStaticArrayType( + boolType, + uint16(128), + ), + uint16(256), + ), + testType: "static array", + expected: "bool[128][256]", + }, + // tuple type + { + input: Type{ + abiTypeID: Tuple, + childTypes: []Type{ + { + abiTypeID: Uint, + bitSize: uint16(32), + }, + { + abiTypeID: Tuple, + childTypes: []Type{ + addressType, + byteType, + makeStaticArrayType(boolType, uint16(10)), + makeDynamicArrayType( + Type{ + abiTypeID: Ufixed, + bitSize: uint16(256), + precision: uint16(10), + }, + ), + }, + staticLength: 4, + }, + makeDynamicArrayType(byteType), + }, + staticLength: 3, + }, + testType: "tuple type", + expected: "(uint32,(address,byte,bool[10],ufixed256x10[]),byte[])", + }, + } + for _, testcase := range testcases { + t.Run(fmt.Sprintf("MakeType test %s", testcase.testType), func(t *testing.T) { + actual := testcase.input.String() + require.Equal(t, testcase.expected, actual, + "MakeType: expected %s, actual %s", testcase.expected, actual) + }) + } +} + +func TestMakeTypeInvalid(t *testing.T) { + // uint + for i := 0; i <= 1000; i++ { + randInput := rand.Uint32() % (1 << 16) + for randInput%8 == 0 && randInput <= 512 && randInput >= 8 { + randInput = rand.Uint32() % (1 << 16) + } + // note: if a var mod 8 = 0 (or not) in uint32, then it should mod 8 = 0 (or not) in uint16. + _, err := makeUintType(int(randInput)) + require.Error(t, err, "makeUintType: should throw error on bitSize input %d", uint16(randInput)) + } + // ufixed + for i := 0; i <= 10000; i++ { + randSize := rand.Uint64() % (1 << 16) + for randSize%8 == 0 && randSize <= 512 && randSize >= 8 { + randSize = rand.Uint64() % (1 << 16) + } + randPrecision := rand.Uint32() + for randPrecision >= 1 && randPrecision <= 160 { + randPrecision = rand.Uint32() + } + _, err := makeUfixedType(int(randSize), int(randPrecision)) + require.Error(t, err, "makeUfixedType: should throw error on bitSize %d, precision %d", randSize, randPrecision) + } +} + +func TestTypeFromStringValid(t *testing.T) { + // uint + for i := 8; i <= 512; i += 8 { + expected, err := makeUintType(i) + require.NoError(t, err, "make uint type in valid space should not return error") + actual, err := TypeOf(expected.String()) + require.NoError(t, err, "TypeOf: uint parsing error: %s", expected.String()) + require.Equal(t, expected, actual, + "TypeOf: expected %s, actual %s", expected.String(), actual.String()) + } + // ufixed + for i := 8; i <= 512; i += 8 { + for j := 1; j <= 160; j++ { + expected, err := makeUfixedType(i, j) + require.NoError(t, err, "make ufixed type in valid space should not return error") + actual, err := TypeOf("ufixed" + strconv.Itoa(i) + "x" + strconv.Itoa(j)) + require.NoError(t, err, "TypeOf ufixed parsing error: %s", expected.String()) + require.Equal(t, expected, actual, + "TypeOf ufixed: expected %s, actual %s", expected.String(), actual.String()) + } + } + var testcases = []struct { + input string + testType string + expected Type + }{ + {input: boolType.String(), testType: "bool", expected: boolType}, + {input: stringType.String(), testType: "string", expected: stringType}, + {input: addressType.String(), testType: "address", expected: addressType}, + {input: byteType.String(), testType: "byte", expected: byteType}, + { + input: "uint256[]", + testType: "dynamic array", + expected: makeDynamicArrayType(Type{abiTypeID: Uint, bitSize: 256}), + }, + { + input: "ufixed256x64[]", + testType: "dynamic array", + expected: makeDynamicArrayType( + Type{ + abiTypeID: Ufixed, + bitSize: 256, + precision: 64, + }, + ), + }, + { + input: "byte[][][][]", + testType: "dynamic array", + expected: makeDynamicArrayType( + makeDynamicArrayType( + makeDynamicArrayType( + makeDynamicArrayType( + byteType, + ), + ), + ), + ), + }, + // static array + { + input: "address[100]", + testType: "static array", + expected: makeStaticArrayType( + addressType, + uint16(100), + ), + }, + { + input: "uint64[][200]", + testType: "static array", + expected: makeStaticArrayType( + makeDynamicArrayType( + Type{abiTypeID: Uint, bitSize: uint16(64)}, + ), + uint16(200), + ), + }, + // tuple type + { + input: "()", + testType: "tuple type", + expected: Type{ + abiTypeID: Tuple, + childTypes: []Type{}, + staticLength: 0, + }, + }, + { + input: "(uint32,(address,byte,bool[10],ufixed256x10[]),byte[])", + testType: "tuple type", + expected: Type{ + abiTypeID: Tuple, + childTypes: []Type{ + { + abiTypeID: Uint, + bitSize: uint16(32), + }, + { + abiTypeID: Tuple, + childTypes: []Type{ + addressType, + byteType, + makeStaticArrayType(boolType, uint16(10)), + makeDynamicArrayType( + Type{ + abiTypeID: Ufixed, + bitSize: uint16(256), + precision: uint16(10), + }, + ), + }, + staticLength: 4, + }, + makeDynamicArrayType(byteType), + }, + staticLength: 3, + }, + }, + { + input: "(uint32,(address,byte,bool[10],(ufixed256x10[])))", + testType: "tuple type", + expected: Type{ + abiTypeID: Tuple, + childTypes: []Type{ + { + abiTypeID: Uint, + bitSize: uint16(32), + }, + { + abiTypeID: Tuple, + childTypes: []Type{ + addressType, + byteType, + makeStaticArrayType(boolType, uint16(10)), + { + abiTypeID: Tuple, + childTypes: []Type{ + makeDynamicArrayType( + Type{ + abiTypeID: Ufixed, + bitSize: uint16(256), + precision: uint16(10), + }, + ), + }, + staticLength: 1, + }, + }, + staticLength: 4, + }, + }, + staticLength: 2, + }, + }, + { + input: "((uint32),(address,(byte,bool[10],ufixed256x10[])))", + testType: "tuple type", + expected: Type{ + abiTypeID: Tuple, + childTypes: []Type{ + { + abiTypeID: Tuple, + childTypes: []Type{ + { + abiTypeID: Uint, + bitSize: uint16(32), + }, + }, + staticLength: 1, + }, + { + abiTypeID: Tuple, + childTypes: []Type{ + addressType, + { + abiTypeID: Tuple, + childTypes: []Type{ + byteType, + makeStaticArrayType(boolType, uint16(10)), + makeDynamicArrayType( + Type{ + abiTypeID: Ufixed, + bitSize: uint16(256), + precision: uint16(10), + }, + ), + }, + staticLength: 3, + }, + }, + staticLength: 2, + }, + }, + staticLength: 2, + }, + }, + } + for _, testcase := range testcases { + t.Run(fmt.Sprintf("TypeOf test %s", testcase.testType), func(t *testing.T) { + actual, err := TypeOf(testcase.input) + require.NoError(t, err, "TypeOf %s parsing error", testcase.testType) + require.Equal(t, testcase.expected, actual, "TestFromString %s: expected %s, actual %s", + testcase.testType, testcase.expected.String(), actual.String()) + }) + } +} + +func TestTypeFromStringInvalid(t *testing.T) { + for i := 0; i <= 1000; i++ { + randSize := rand.Uint64() + for randSize%8 == 0 && randSize <= 512 && randSize >= 8 { + randSize = rand.Uint64() + } + errorInput := "uint" + strconv.FormatUint(randSize, 10) + _, err := TypeOf(errorInput) + require.Error(t, err, "makeUintType: should throw error on bitSize input %d", randSize) + } + for i := 0; i <= 10000; i++ { + randSize := rand.Uint64() + for randSize%8 == 0 && randSize <= 512 && randSize >= 8 { + randSize = rand.Uint64() + } + randPrecision := rand.Uint64() + for randPrecision >= 1 && randPrecision <= 160 { + randPrecision = rand.Uint64() + } + errorInput := "ufixed" + strconv.FormatUint(randSize, 10) + "x" + strconv.FormatUint(randPrecision, 10) + _, err := TypeOf(errorInput) + require.Error(t, err, "makeUintType: should throw error on bitSize input %d", randSize) + } + var testcases = []string{ + // uint + "uint123x345", + "uint 128", + "uint8 ", + "uint!8", + "uint[32]", + "uint-893", + "uint#120\\", + // ufixed + "ufixed000000000016x0000010", + "ufixed123x345", + "ufixed 128 x 100", + "ufixed64x10 ", + "ufixed!8x2 ", + "ufixed[32]x16", + "ufixed-64x+100", + "ufixed16x+12", + // dynamic array + "uint256 []", + "byte[] ", + "[][][]", + "stuff[]", + // static array + "ufixed32x10[0]", + "byte[10 ]", + "uint64[0x21]", + // tuple + "(ufixed128x10))", + "(,uint128,byte[])", + "(address,ufixed64x5,)", + "(byte[16],somethingwrong)", + "( )", + "((uint32)", + "(byte,,byte)", + "((byte),,(byte))", + } + for _, testcase := range testcases { + t.Run(fmt.Sprintf("TypeOf dynamic array test %s", testcase), func(t *testing.T) { + _, err := TypeOf(testcase) + require.Error(t, err, "%s should throw error", testcase) + }) + } +} + +func generateTupleType(baseTypes []Type, tupleTypes []Type) Type { + if len(baseTypes) == 0 && len(tupleTypes) == 0 { + panic("should not pass all nil arrays into generateTupleType") + } + tupleLen := 0 + for tupleLen == 0 { + tupleLen = rand.Intn(20) + } + resultTypes := make([]Type, tupleLen) + for i := 0; i < tupleLen; i++ { + baseOrTuple := rand.Intn(5) + if baseOrTuple == 1 && len(tupleTypes) > 0 { + resultTypes[i] = tupleTypes[rand.Intn(len(tupleTypes))] + } else { + resultTypes[i] = baseTypes[rand.Intn(len(baseTypes))] + } + } + return Type{abiTypeID: Tuple, childTypes: resultTypes, staticLength: uint16(tupleLen)} +} + +func TestTypeMISC(t *testing.T) { + rand.Seed(time.Now().Unix()) + + var testpool = []Type{ + boolType, + addressType, + stringType, + byteType, + } + for i := 8; i <= 512; i += 8 { + uintT, err := makeUintType(i) + require.NoError(t, err, "make uint type error") + testpool = append(testpool, uintT) + } + for i := 8; i <= 512; i += 8 { + for j := 1; j <= 160; j++ { + ufixedT, err := makeUfixedType(i, j) + require.NoError(t, err, "make ufixed type error: bitSize %d, precision %d", i, j) + testpool = append(testpool, ufixedT) + } + } + for _, testcase := range testpool { + testpool = append(testpool, makeDynamicArrayType(testcase)) + testpool = append(testpool, makeStaticArrayType(testcase, 10)) + testpool = append(testpool, makeStaticArrayType(testcase, 20)) + } + + for _, testcase := range testpool { + require.True(t, testcase.Equal(testcase), "test type self equal error") + } + baseTestCount := 0 + for baseTestCount < 1000 { + index0 := rand.Intn(len(testpool)) + index1 := rand.Intn(len(testpool)) + if index0 == index1 { + continue + } + require.False(t, testpool[index0].Equal(testpool[index1]), + "test type not equal error\n%s\n%s", + testpool[index0].String(), testpool[index1].String()) + baseTestCount++ + } + + testpoolTuple := make([]Type, 0) + for i := 0; i < 100; i++ { + testpoolTuple = append(testpoolTuple, generateTupleType(testpool, testpoolTuple)) + } + for _, testcaseTuple := range testpoolTuple { + require.True(t, testcaseTuple.Equal(testcaseTuple), "test type tuple equal error") + } + + tupleTestCount := 0 + for tupleTestCount < 100 { + index0 := rand.Intn(len(testpoolTuple)) + index1 := rand.Intn(len(testpoolTuple)) + if testpoolTuple[index0].String() == testpoolTuple[index1].String() { + continue + } + require.False(t, testpoolTuple[index0].Equal(testpoolTuple[index1]), + "test type tuple not equal error\n%s\n%s", + testpoolTuple[index0].String(), testpoolTuple[index1].String()) + tupleTestCount++ + } + + testpool = append(testpool, testpoolTuple...) + isDynamicCount := 0 + for isDynamicCount < 100 { + index := rand.Intn(len(testpool)) + isDynamicArr := strings.Contains(testpool[index].String(), "[]") + isDynamicStr := strings.Contains(testpool[index].String(), "string") + require.Equal(t, isDynamicArr || isDynamicStr, testpool[index].IsDynamic(), + "test type isDynamic error\n%s", testpool[index].String()) + isDynamicCount++ + } + + addressByteLen, err := addressType.ByteLen() + require.NoError(t, err, "address type bytelen should not return error") + require.Equal(t, 32, addressByteLen, "address type bytelen should be 32") + byteByteLen, err := byteType.ByteLen() + require.NoError(t, err, "byte type bytelen should not return error") + require.Equal(t, 1, byteByteLen, "byte type bytelen should be 1") + boolByteLen, err := boolType.ByteLen() + require.NoError(t, err, "bool type bytelen should be 1") + require.Equal(t, 1, boolByteLen, "bool type bytelen should be 1") + + byteLenTestCount := 0 + for byteLenTestCount < 100 { + index := rand.Intn(len(testpool)) + testType := testpool[index] + byteLen, err := testType.ByteLen() + if testType.IsDynamic() { + require.Error(t, err, "byteLen test error on %s dynamic type, should have error", + testType.String()) + } else { + require.NoError(t, err, "byteLen test error on %s dynamic type, should not have error") + if testType.abiTypeID == Tuple { + sizeSum := 0 + for i := 0; i < len(testType.childTypes); i++ { + if testType.childTypes[i].abiTypeID == Bool { + // search previous bool + before := findBoolLR(testType.childTypes, i, -1) + // search after bool + after := findBoolLR(testType.childTypes, i, 1) + // append to heads and tails + require.True(t, before%8 == 0, "expected tuple bool compact by 8") + if after > 7 { + after = 7 + } + i += after + sizeSum++ + } else { + childByteSize, err := testType.childTypes[i].ByteLen() + require.NoError(t, err, "byteLen not expected to fail on tuple child type") + sizeSum += childByteSize + } + } + + require.Equal(t, sizeSum, byteLen, + "%s do not match calculated byte length %d", testType.String(), sizeSum) + } else if testType.abiTypeID == ArrayStatic { + if testType.childTypes[0].abiTypeID == Bool { + expected := testType.staticLength / 8 + if testType.staticLength%8 != 0 { + expected++ + } + actual, err := testType.ByteLen() + require.NoError(t, err, "%s should not return error on byteLen test") + require.Equal(t, int(expected), actual, "%s do not match calculated byte length %d", + testType.String(), expected) + } else { + childSize, err := testType.childTypes[0].ByteLen() + require.NoError(t, err, "%s should not return error on byteLen test", testType.childTypes[0].String()) + expected := childSize * int(testType.staticLength) + require.Equal(t, expected, byteLen, + "%s do not match calculated byte length %d", testType.String(), expected) + } + } + } + byteLenTestCount++ + } +} diff --git a/go.mod b/go.mod index a154993..d56abdb 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,14 @@ module github.com/algorand/avm-abi go 1.17 + +require ( + github.com/chrismcguire/gobberish v0.0.0-20150821175641-1d8adb509a0e + github.com/stretchr/testify v1.8.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..116cf0d --- /dev/null +++ b/go.sum @@ -0,0 +1,17 @@ +github.com/chrismcguire/gobberish v0.0.0-20150821175641-1d8adb509a0e h1:CHPYEbz71w8DqJ7DRIq+MXyCQsdibK08vdcQTY4ufas= +github.com/chrismcguire/gobberish v0.0.0-20150821175641-1d8adb509a0e/go.mod h1:6Xhs0ZlsRjXLIiSMLKafbZxML/j30pg9Z1priLuha5s= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From c7fad312651c50f5e526194bd1f38f1bf7fb9ad3 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Mon, 8 Aug 2022 16:19:17 -0700 Subject: [PATCH 2/7] Downgrade testify to match go-algorand's version (#3) --- go.mod | 4 ++-- go.sum | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index d56abdb..35d85eb 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,11 @@ go 1.17 require ( github.com/chrismcguire/gobberish v0.0.0-20150821175641-1d8adb509a0e - github.com/stretchr/testify v1.8.0 + github.com/stretchr/testify v1.7.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/go.sum b/go.sum index 116cf0d..bf11513 100644 --- a/go.sum +++ b/go.sum @@ -6,12 +6,9 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 10a02795cc8b6da60d85b8b5f49607f3d9578857 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Tue, 9 Aug 2022 11:41:03 -0700 Subject: [PATCH 3/7] Use `avm_abi` package name --- avm-abi/encode.go | 2 +- avm-abi/encode_test.go | 2 +- avm-abi/json.go | 2 +- avm-abi/json_test.go | 2 +- avm-abi/type.go | 2 +- avm-abi/type_test.go | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/avm-abi/encode.go b/avm-abi/encode.go index 9d1b9d8..cb9893b 100644 --- a/avm-abi/encode.go +++ b/avm-abi/encode.go @@ -1,4 +1,4 @@ -package abi +package avm_abi import ( "encoding/binary" diff --git a/avm-abi/encode_test.go b/avm-abi/encode_test.go index 9dbaa3c..ea321bc 100644 --- a/avm-abi/encode_test.go +++ b/avm-abi/encode_test.go @@ -1,4 +1,4 @@ -package abi +package avm_abi import ( "crypto/rand" diff --git a/avm-abi/json.go b/avm-abi/json.go index 73cf0bc..b459aaf 100644 --- a/avm-abi/json.go +++ b/avm-abi/json.go @@ -1,4 +1,4 @@ -package abi +package avm_abi import ( "bytes" diff --git a/avm-abi/json_test.go b/avm-abi/json_test.go index ab489e9..dab0bee 100644 --- a/avm-abi/json_test.go +++ b/avm-abi/json_test.go @@ -1,4 +1,4 @@ -package abi +package avm_abi import ( "fmt" diff --git a/avm-abi/type.go b/avm-abi/type.go index 474d7e4..c8d3e33 100644 --- a/avm-abi/type.go +++ b/avm-abi/type.go @@ -1,4 +1,4 @@ -package abi +package avm_abi import ( "fmt" diff --git a/avm-abi/type_test.go b/avm-abi/type_test.go index bfce2b2..ed4a4e6 100644 --- a/avm-abi/type_test.go +++ b/avm-abi/type_test.go @@ -1,4 +1,4 @@ -package abi +package avm_abi import ( "fmt" From f1e76f712ca7fca6b28fb11d3c6546a499059aae Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Tue, 9 Aug 2022 14:09:39 -0700 Subject: [PATCH 4/7] Revert "Use `avm_abi` package name" This reverts commit 10a02795cc8b6da60d85b8b5f49607f3d9578857. --- avm-abi/encode.go | 2 +- avm-abi/encode_test.go | 2 +- avm-abi/json.go | 2 +- avm-abi/json_test.go | 2 +- avm-abi/type.go | 2 +- avm-abi/type_test.go | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/avm-abi/encode.go b/avm-abi/encode.go index cb9893b..9d1b9d8 100644 --- a/avm-abi/encode.go +++ b/avm-abi/encode.go @@ -1,4 +1,4 @@ -package avm_abi +package abi import ( "encoding/binary" diff --git a/avm-abi/encode_test.go b/avm-abi/encode_test.go index ea321bc..9dbaa3c 100644 --- a/avm-abi/encode_test.go +++ b/avm-abi/encode_test.go @@ -1,4 +1,4 @@ -package avm_abi +package abi import ( "crypto/rand" diff --git a/avm-abi/json.go b/avm-abi/json.go index b459aaf..73cf0bc 100644 --- a/avm-abi/json.go +++ b/avm-abi/json.go @@ -1,4 +1,4 @@ -package avm_abi +package abi import ( "bytes" diff --git a/avm-abi/json_test.go b/avm-abi/json_test.go index dab0bee..ab489e9 100644 --- a/avm-abi/json_test.go +++ b/avm-abi/json_test.go @@ -1,4 +1,4 @@ -package avm_abi +package abi import ( "fmt" diff --git a/avm-abi/type.go b/avm-abi/type.go index c8d3e33..474d7e4 100644 --- a/avm-abi/type.go +++ b/avm-abi/type.go @@ -1,4 +1,4 @@ -package avm_abi +package abi import ( "fmt" diff --git a/avm-abi/type_test.go b/avm-abi/type_test.go index ed4a4e6..bfce2b2 100644 --- a/avm-abi/type_test.go +++ b/avm-abi/type_test.go @@ -1,4 +1,4 @@ -package avm_abi +package abi import ( "fmt" From 55ca7fad237a2d22bef01e7bf2061ec5a21e6099 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Tue, 9 Aug 2022 14:17:28 -0700 Subject: [PATCH 5/7] Rename directory to abi --- {avm-abi => abi}/encode.go | 0 {avm-abi => abi}/encode_test.go | 0 {avm-abi => abi}/json.go | 0 {avm-abi => abi}/json_test.go | 0 {avm-abi => abi}/type.go | 0 {avm-abi => abi}/type_test.go | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename {avm-abi => abi}/encode.go (100%) rename {avm-abi => abi}/encode_test.go (100%) rename {avm-abi => abi}/json.go (100%) rename {avm-abi => abi}/json_test.go (100%) rename {avm-abi => abi}/type.go (100%) rename {avm-abi => abi}/type_test.go (100%) diff --git a/avm-abi/encode.go b/abi/encode.go similarity index 100% rename from avm-abi/encode.go rename to abi/encode.go diff --git a/avm-abi/encode_test.go b/abi/encode_test.go similarity index 100% rename from avm-abi/encode_test.go rename to abi/encode_test.go diff --git a/avm-abi/json.go b/abi/json.go similarity index 100% rename from avm-abi/json.go rename to abi/json.go diff --git a/avm-abi/json_test.go b/abi/json_test.go similarity index 100% rename from avm-abi/json_test.go rename to abi/json_test.go diff --git a/avm-abi/type.go b/abi/type.go similarity index 100% rename from avm-abi/type.go rename to abi/type.go diff --git a/avm-abi/type_test.go b/abi/type_test.go similarity index 100% rename from avm-abi/type_test.go rename to abi/type_test.go From fa0e08222b9e999ef70d97736833d9d20fd3a25f Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Wed, 10 Aug 2022 09:34:14 -0700 Subject: [PATCH 6/7] Add `golangci-lint` (#4) * Add golangci-lint * Checkout code first * Lint tests --- .github/workflows/golangci-lint.yml | 28 +++++++++++++++++++++ .golangci-warnings.yml | 39 +++++++++++++++++++++++++++++ .golangci.yml | 38 ++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 .github/workflows/golangci-lint.yml create mode 100644 .golangci-warnings.yml create mode 100644 .golangci.yml diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..57704d0 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,28 @@ +name: golangci-lint +on: + pull_request: +permissions: + contents: read + # Optional: allow read access to pull request. Use with `only-new-issues` option. + # pull-requests: read +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version-file: 'go.mod' + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version + version: v1.41.1 + - name: golangci-lint warnings + uses: golangci/golangci-lint-action@v3 + with: + # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version + version: v1.41.1 + # Optional: golangci-lint command line arguments. + args: -c .golangci-warnings.yml diff --git a/.golangci-warnings.yml b/.golangci-warnings.yml new file mode 100644 index 0000000..598c897 --- /dev/null +++ b/.golangci-warnings.yml @@ -0,0 +1,39 @@ +run: + timeout: 5m + tests: true + +linters: + disable-all: true + enable: + - staticcheck + - structcheck + - typecheck + - varcheck + - deadcode + - gosimple + - unused + +severity: + default-severity: warning + +issues: + # Disable default exclude rules listed in `golangci-lint run --help` (selectively re-enable some below) + exclude-use-default: false + + # Maximum issues count per one linter. Set to 0 to disable. Default is 50. + max-issues-per-linter: 0 + + # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. + max-same-issues: 0 + + exclude: + # ignore govet false positive fixed in https://github.com/golang/go/issues/45043 + - "sigchanyzer: misuse of unbuffered os.Signal channel as argument to signal.Notify" + # ignore golint false positive fixed in https://github.com/golang/lint/pull/487 + - "exported method (.*).Unwrap` should have comment or be unexported" + + # Enable some golangci-lint default exception rules: + # "EXC0001 errcheck: Almost all programs ignore errors on these functions and in most cases it's ok" + - Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). is not checked + # "EXC0005 staticcheck: Developers tend to write in C-style with an explicit 'break' in a 'switch', so it's ok to ignore" + - ineffective break statement. Did you mean to break out of the outer loop diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..3f8cc03 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,38 @@ +run: + timeout: 5m + tests: true + +linters: + disable-all: true + enable: + - errcheck + - gofmt + - golint + - govet + - ineffassign + - misspell + +severity: + default-severity: error + +issues: + # Disable default exclude rules listed in `golangci-lint run --help` (selectively re-enable some below) + exclude-use-default: false + + # Maximum issues count per one linter. Set to 0 to disable. Default is 50. + max-issues-per-linter: 0 + + # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. + max-same-issues: 0 + + exclude: + # ignore govet false positive fixed in https://github.com/golang/go/issues/45043 + - "sigchanyzer: misuse of unbuffered os.Signal channel as argument to signal.Notify" + # ignore golint false positive fixed in https://github.com/golang/lint/pull/487 + - "exported method (.*).Unwrap` should have comment or be unexported" + + # Enable some golangci-lint default exception rules: + # "EXC0001 errcheck: Almost all programs ignore errors on these functions and in most cases it's ok" + - Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). is not checked + # "EXC0005 staticcheck: Developers tend to write in C-style with an explicit 'break' in a 'switch', so it's ok to ignore" + - ineffective break statement. Did you mean to break out of the outer loop From 4e3333e8982866f11ee5e3955ca99dea6c2e647f Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Wed, 10 Aug 2022 09:36:40 -0700 Subject: [PATCH 7/7] Code changes (#2) * Make all txn types into variables * Remove ParseArgJSONtoByteSlice * Use '%w' for errors * Remove no longer relevent comment * Test VerifyMethodSignature * Add documentation * Make readme match docs * Update docs package * Update abi/doc.go Co-authored-by: Michael Diamant Co-authored-by: Michael Diamant --- README.md | 2 +- abi/doc.go | 14 ++++ abi/encode.go | 79 ++------------------- abi/encode_test.go | 167 ++++++++++++++------------------------------- abi/json.go | 3 - abi/type.go | 34 ++++++++- 6 files changed, 104 insertions(+), 195 deletions(-) create mode 100644 abi/doc.go diff --git a/README.md b/README.md index 63836dc..149c15a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # avm-abi -A reference implementation for the Algorand [ARC-4](https://arc.algorand.foundation/ARCs/arc-0004) ABI type system. +An implementation of the Algorand [ARC-4](https://arc.algorand.foundation/ARCs/arc-0004) ABI type system. diff --git a/abi/doc.go b/abi/doc.go new file mode 100644 index 0000000..da680ab --- /dev/null +++ b/abi/doc.go @@ -0,0 +1,14 @@ +/* +Package abi provides an implementation of the Algorand ARC-4 ABI type system. + +See https://arc.algorand.foundation/ARCs/arc-0004 for the corresponding specification. + + +Basic Operations + +This package can parse ABI type names using the `abi.TypeOf()` function. + +`abi.TypeOf()` returns an `abi.Type` struct. The `abi.Type` struct's `Encode` and `Decode` methods +can convert between Go values and encoded ABI byte strings. +*/ +package abi diff --git a/abi/encode.go b/abi/encode.go index 9d1b9d8..ea0f2b0 100644 --- a/abi/encode.go +++ b/abi/encode.go @@ -2,7 +2,6 @@ package abi import ( "encoding/binary" - "encoding/json" "fmt" "math/big" "reflect" @@ -459,78 +458,12 @@ func decodeTuple(encoded []byte, childT []Type) ([]interface{}, error) { return values, nil } -// maxAppArgs is the maximum number of arguments for an application call transaction, in compliance -// with ARC-4. Currently this is the same as the MaxAppArgs consensus parameter, but the -// difference is that the consensus parameter is liable to change in a future consensus upgrade. -// However, the ARC-4 ABI argument encoding **MUST** always remain the same. -const maxAppArgs = 16 - -// The tuple threshold is maxAppArgs, minus 1 for the method selector in the first app arg, -// minus 1 for the final app argument becoming a tuple of the remaining method args -const methodArgsTupleThreshold = maxAppArgs - 2 - -// ParseArgJSONtoByteSlice convert input method arguments to ABI encoded bytes -// it converts funcArgTypes into a tuple type and apply changes over input argument string (in JSON format) -// if there are greater or equal to 15 inputs, then we compact the tailing inputs into one tuple -func ParseArgJSONtoByteSlice(argTypes []string, jsonArgs []string, applicationArgs *[][]byte) error { - abiTypes := make([]Type, len(argTypes)) - for i, typeString := range argTypes { - abiType, err := TypeOf(typeString) - if err != nil { - return err - } - abiTypes[i] = abiType - } - - if len(abiTypes) != len(jsonArgs) { - return fmt.Errorf("input argument number %d != method argument number %d", len(jsonArgs), len(abiTypes)) - } - - // Up to 16 app arguments can be passed to app call. First is reserved for method selector, - // and the rest are for method call arguments. But if more than 15 method call arguments - // are present, then the method arguments after the 14th are placed in a tuple in the last - // app argument slot - if len(abiTypes) > maxAppArgs-1 { - typesForTuple := make([]Type, len(abiTypes)-methodArgsTupleThreshold) - copy(typesForTuple, abiTypes[methodArgsTupleThreshold:]) - - compactedType, err := MakeTupleType(typesForTuple) - if err != nil { - return err - } - - abiTypes = append(abiTypes[:methodArgsTupleThreshold], compactedType) - - tupleValues := make([]json.RawMessage, len(jsonArgs)-methodArgsTupleThreshold) - for i, jsonArg := range jsonArgs[methodArgsTupleThreshold:] { - tupleValues[i] = []byte(jsonArg) - } - - remainingJSON, err := json.Marshal(tupleValues) - if err != nil { - return err - } - - jsonArgs = append(jsonArgs[:methodArgsTupleThreshold], string(remainingJSON)) - } - - // parse JSON value to ABI encoded bytes - for i := 0; i < len(jsonArgs); i++ { - interfaceVal, err := abiTypes[i].UnmarshalFromJSON([]byte(jsonArgs[i])) - if err != nil { - return err - } - abiEncoded, err := abiTypes[i].Encode(interfaceVal) - if err != nil { - return err - } - *applicationArgs = append(*applicationArgs, abiEncoded) - } - return nil -} - // ParseMethodSignature parses a method of format `method(argType1,argType2,...)retType` // into `method` {`argType1`,`argType2`,...} and `retType` +// +// NOTE: This function **DOES NOT** verify that the argument or return type strings represent valid +// ABI types. Consider using `VerifyMethodSignature` prior to calling this function if you wish to +// verify those types. func ParseMethodSignature(methodSig string) (name string, argTypes []string, returnType string, err error) { argsStart := strings.Index(methodSig, "(") if argsStart == -1 { @@ -586,14 +519,14 @@ func VerifyMethodSignature(methodSig string) error { _, err = TypeOf(argType) if err != nil { - return fmt.Errorf("Error parsing argument type at index %d: %s", i, err.Error()) + return fmt.Errorf("Error parsing argument type at index %d: %w", i, err) } } if retType != VoidReturnType { _, err = TypeOf(retType) if err != nil { - return fmt.Errorf("Error parsing return type: %s", err.Error()) + return fmt.Errorf("Error parsing return type: %w", err) } } diff --git a/abi/encode_test.go b/abi/encode_test.go index 9dbaa3c..7cd3d60 100644 --- a/abi/encode_test.go +++ b/abi/encode_test.go @@ -3,7 +3,6 @@ package abi import ( "crypto/rand" "encoding/binary" - "fmt" "math/big" "testing" @@ -1030,122 +1029,6 @@ func TestRandomABIEncodeDecodeRoundTrip(t *testing.T) { categorySelfRoundTripTest(t, testValuePool[Tuple]) } -func TestParseArgJSONtoByteSlice(t *testing.T) { - makeRepeatSlice := func(size int, value string) []string { - slice := make([]string, size) - for i := range slice { - slice[i] = value - } - return slice - } - - tests := []struct { - argTypes []string - jsonArgs []string - expectedAppArgs [][]byte - }{ - { - argTypes: []string{}, - jsonArgs: []string{}, - expectedAppArgs: [][]byte{}, - }, - { - argTypes: []string{"uint8"}, - jsonArgs: []string{"100"}, - expectedAppArgs: [][]byte{{100}}, - }, - { - argTypes: []string{"uint8", "uint16"}, - jsonArgs: []string{"100", "65535"}, - expectedAppArgs: [][]byte{{100}, {255, 255}}, - }, - { - argTypes: makeRepeatSlice(15, "string"), - jsonArgs: []string{ - `"a"`, - `"b"`, - `"c"`, - `"d"`, - `"e"`, - `"f"`, - `"g"`, - `"h"`, - `"i"`, - `"j"`, - `"k"`, - `"l"`, - `"m"`, - `"n"`, - `"o"`, - }, - expectedAppArgs: [][]byte{ - {00, 01, 97}, - {00, 01, 98}, - {00, 01, 99}, - {00, 01, 100}, - {00, 01, 101}, - {00, 01, 102}, - {00, 01, 103}, - {00, 01, 104}, - {00, 01, 105}, - {00, 01, 106}, - {00, 01, 107}, - {00, 01, 108}, - {00, 01, 109}, - {00, 01, 110}, - {00, 01, 111}, - }, - }, - { - argTypes: makeRepeatSlice(16, "string"), - jsonArgs: []string{ - `"a"`, - `"b"`, - `"c"`, - `"d"`, - `"e"`, - `"f"`, - `"g"`, - `"h"`, - `"i"`, - `"j"`, - `"k"`, - `"l"`, - `"m"`, - `"n"`, - `"o"`, - `"p"`, - }, - expectedAppArgs: [][]byte{ - {00, 01, 97}, - {00, 01, 98}, - {00, 01, 99}, - {00, 01, 100}, - {00, 01, 101}, - {00, 01, 102}, - {00, 01, 103}, - {00, 01, 104}, - {00, 01, 105}, - {00, 01, 106}, - {00, 01, 107}, - {00, 01, 108}, - {00, 01, 109}, - {00, 01, 110}, - {00, 04, 00, 07, 00, 01, 111, 00, 01, 112}, - }, - }, - } - - for i, test := range tests { - t.Run(fmt.Sprintf("index=%d", i), func(t *testing.T) { - applicationArgs := [][]byte{} - err := ParseArgJSONtoByteSlice(test.argTypes, test.jsonArgs, &applicationArgs) - require.NoError(t, err) - require.Equal(t, test.expectedAppArgs, applicationArgs) - }) - } -} - func TestParseMethodSignature(t *testing.T) { tests := []struct { signature string @@ -1196,6 +1079,56 @@ func TestParseMethodSignature(t *testing.T) { } } +func TestVerifyMethodSignature(t *testing.T) { + tests := []struct { + method string + pass bool + }{ + { + method: "abc(uint64)void", + pass: true, + }, + { + method: "all_special_args(txn,pay,keyreg,acfg,axfer,afrz,appl,account,application,asset)void", + pass: true, + }, + { + method: "abc(uint64)", + pass: false, + }, + { + method: "abc(uint65)void", + pass: false, + }, + { + method: "(uint64)void", + pass: false, + }, + { + method: "abc(uint65,void", + pass: false, + }, + { + method: "abc(uint64))void", + pass: false, + }, + { + method: "abc", + pass: false, + }, + } + + for _, test := range tests { + err := VerifyMethodSignature(test.method) + + if test.pass { + require.NoErrorf(t, err, `Unexpected error from method "%s"`, test.method) + } else { + require.Error(t, err, `Expected an error from method "%s"`, test.method) + } + } +} + func TestInferToSlice(t *testing.T) { var emptySlice []int tests := []struct { diff --git a/abi/json.go b/abi/json.go index 73cf0bc..621d572 100644 --- a/abi/json.go +++ b/abi/json.go @@ -9,9 +9,6 @@ import ( "math/big" ) -// NOTE: discussion about go-algorand-sdk -// https://github.com/algorand/go-algorand/pull/3375#issuecomment-1007536841 - var base32Encoder = base32.StdEncoding.WithPadding(base32.NoPadding) func addressCheckSum(addressBytes [addressByteSize]byte) []byte { diff --git a/abi/type.go b/abi/type.go index 474d7e4..39e6e35 100644 --- a/abi/type.go +++ b/abi/type.go @@ -108,6 +108,9 @@ var ufixedRegexp = regexp.MustCompile(`^ufixed([1-9][\d]*)x([1-9][\d]*)$`) // TypeOf parses an ABI type string. // For example: `TypeOf("(uint64,byte[])")` +// +// Note: this function only supports "basic" ABI types. Reference types and transaction types are +// not supported and will produce an error. func TypeOf(str string) (Type, error) { switch { case strings.HasSuffix(str, "[]"): @@ -447,11 +450,40 @@ func (t Type) ByteLen() (int, error) { // AnyTransactionType is the ABI argument type string for a nonspecific transaction argument const AnyTransactionType = "txn" +// PaymentTransactionType is the ABI argument type string for a payment transaction argument +const PaymentTransactionType = "pay" + +// KeyRegistrationTransactionType is the ABI argument type string for a key registration transaction +// argument +const KeyRegistrationTransactionType = "keyreg" + +// AssetConfigTransactionType is the ABI argument type string for an asset configuration transaction +// argument +const AssetConfigTransactionType = "acfg" + +// AssetTransferTransactionType is the ABI argument type string for an asset transfer transaction +// argument +const AssetTransferTransactionType = "axfer" + +// AssetFreezeTransactionType is the ABI argument type string for an asset freeze transaction +// argument +const AssetFreezeTransactionType = "afrz" + +// ApplicationCallTransactionType is the ABI argument type string for an application call +// transaction argument +const ApplicationCallTransactionType = "appl" + // IsTransactionType checks if a type string represents a transaction type // argument, such as "txn", "pay", "keyreg", etc. func IsTransactionType(s string) bool { switch s { - case AnyTransactionType, "pay", "keyreg", "acfg", "axfer", "afrz", "appl": + case AnyTransactionType, + PaymentTransactionType, + KeyRegistrationTransactionType, + AssetConfigTransactionType, + AssetTransferTransactionType, + AssetFreezeTransactionType, + ApplicationCallTransactionType: return true default: return false