Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Adds box and parsing modules #16

Merged
merged 3 commits into from
Jan 12, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions abi/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import (

var base32Encoder = base32.StdEncoding.WithPadding(base32.NoPadding)

func addressCheckSum(addressBytes [addressByteSize]byte) []byte {
// AddressCheckSum computes the address check sum
func AddressCheckSum(addressBytes [addressByteSize]byte) []byte {
hashed := sha512.Sum512_256(addressBytes[:])
return hashed[addressByteSize-checksumByteSize:]
}

func addressToString(addressBytes [addressByteSize]byte) string {
checksum := addressCheckSum(addressBytes)
// AddressToString converts address to a string
func AddressToString(addressBytes [addressByteSize]byte) string {
checksum := AddressCheckSum(addressBytes)

var addressBytesAndChecksum [addressByteSize + checksumByteSize]byte
copy(addressBytesAndChecksum[:], addressBytes[:])
Expand All @@ -26,7 +28,8 @@ func addressToString(addressBytes [addressByteSize]byte) string {
return base32Encoder.EncodeToString(addressBytesAndChecksum[:])
}

func addressFromString(addressString string) ([addressByteSize]byte, error) {
// AddressFromString converts a string to an address
func AddressFromString(addressString string) ([addressByteSize]byte, error) {
decoded, err := base32Encoder.DecodeString(addressString)
if err != nil {
return [addressByteSize]byte{},
Expand All @@ -43,7 +46,7 @@ func addressFromString(addressString string) ([addressByteSize]byte, error) {
var addressBytes [addressByteSize]byte
copy(addressBytes[:], decoded[:])

checksum := addressCheckSum(addressBytes)
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",
Expand Down Expand Up @@ -116,7 +119,7 @@ func (t Type) MarshalToJSON(value interface{}) ([]byte, error) {
default:
return nil, fmt.Errorf("cannot infer to byte slice/array for marshal to JSON")
}
return json.Marshal(addressToString(addressBytes))
return json.Marshal(AddressToString(addressBytes))
case ArrayStatic, ArrayDynamic:
values, err := inferToSlice(value)
if err != nil {
Expand Down Expand Up @@ -210,7 +213,7 @@ func (t Type) UnmarshalFromJSON(jsonEncoded []byte) (interface{}, error) {
return nil, fmt.Errorf("cannot cast JSON encoded (%s) to address string: %w", string(jsonEncoded), err)
}

addrBytes, err := addressFromString(addrStr)
addrBytes, err := AddressFromString(addrStr)
if err != nil {
return nil, err
}
Expand Down
8 changes: 4 additions & 4 deletions abi/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ func TestAddress(t *testing.T) {

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))
require.Equal(t, testCase.addressChecksum[:], AddressCheckSum(testCase.addressBytes))
require.Equal(t, testCase.addressString, AddressToString(testCase.addressBytes))

actualBytes, err := addressFromString(testCase.addressString)
actualBytes, err := AddressFromString(testCase.addressString)
require.NoError(t, err)
require.Equal(t, testCase.addressBytes, actualBytes)
})
Expand Down Expand Up @@ -77,7 +77,7 @@ func TestAddress(t *testing.T) {

for _, testCase := range testCases {
t.Run(testCase.addressString, func(t *testing.T) {
_, err := addressFromString(testCase.addressString)
_, err := AddressFromString(testCase.addressString)
require.ErrorContains(t, err, testCase.expectedError)
})
}
Expand Down
39 changes: 39 additions & 0 deletions apps/box.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package apps

import (
"encoding/binary"
"fmt"
)

const boxPrefix = "bx:"
const boxPrefixLength = len(boxPrefix)
const boxNameIndex = boxPrefixLength + 8 // len("bx:") + 8 (appIdx, big-endian)

// MakeBoxKey creates the key that a box named `name` under app `appIdx` should use.
func MakeBoxKey(appIdx uint64, name string) string {
/* This format is chosen so that a simple indexing scheme on the key would
allow for quick lookups of all the boxes of a certain app, or even all
the boxes of a certain app with a certain prefix.

The "bx:" prefix is so that the kvstore might be usable for things
besides boxes.
*/
key := make([]byte, boxNameIndex+len(name))
copy(key, boxPrefix)
binary.BigEndian.PutUint64(key[boxPrefixLength:], uint64(appIdx))
copy(key[boxNameIndex:], name)
return string(key)
}

// SplitBoxKey extracts an appid and box name from a string that was created by MakeBoxKey()
func SplitBoxKey(key string) (uint64, string, error) {
if len(key) < boxNameIndex {
return 0, "", fmt.Errorf("SplitBoxKey() cannot extract AppIndex as key (%s) too short (length=%d)", key, len(key))
}
if key[:boxPrefixLength] != boxPrefix {
return 0, "", fmt.Errorf("SplitBoxKey() illegal app box prefix in key (%s). Expected prefix '%s'", key, boxPrefix)
}
keyBytes := []byte(key)
app := binary.BigEndian.Uint64(keyBytes[boxPrefixLength:boxNameIndex])
return app, key[boxNameIndex:], nil
}
48 changes: 48 additions & 0 deletions apps/box_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package apps

import (
"fmt"
"github.com/stretchr/testify/require"
"testing"
)

func TestMakeBoxKey(t *testing.T) {
t.Parallel()

type testCase struct {
description string
name string
app uint64
key string
err string
}

pp := func(tc testCase) string {
return fmt.Sprintf("<<<%s>>> (name, app) = (%#v, %d) --should--> key = %#v (err = [%s])", tc.description, tc.name, tc.app, tc.key, tc.err)
}

var testCases = []testCase{
// COPACETIC:
{"zero appid", "stranger", 0, "bx:\x00\x00\x00\x00\x00\x00\x00\x00stranger", ""},
{"typical", "348-8uj", 131231, "bx:\x00\x00\x00\x00\x00\x02\x00\x9f348-8uj", ""},
{"empty box name", "", 42, "bx:\x00\x00\x00\x00\x00\x00\x00*", ""},
{"random byteslice", "{\xbb\x04\a\xd1\xe2\xc6I\x81{", 13475904583033571713, "bx:\xbb\x04\a\xd1\xe2\xc6I\x81{\xbb\x04\a\xd1\xe2\xc6I\x81{", ""},

// ERRORS:
{"too short", "", 0, "stranger", "SplitBoxKey() cannot extract AppIndex as key (stranger) too short (length=8)"},
{"wrong prefix", "", 0, "strangersINTHEdark", "SplitBoxKey() illegal app box prefix in key (strangersINTHEdark). Expected prefix 'bx:'"},
}

for _, tc := range testCases {
app, name, err := SplitBoxKey(tc.key)

if tc.err == "" {
key := MakeBoxKey(uint64(tc.app), tc.name)
require.Equal(t, uint64(tc.app), app, pp(tc))
require.Equal(t, tc.name, name, pp(tc))
require.Equal(t, tc.key, key, pp(tc))
} else {
require.EqualError(t, err, tc.err, pp(tc))
}
}
}
88 changes: 88 additions & 0 deletions apps/parsing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package apps

import (
"encoding/base32"
"encoding/base64"
"encoding/binary"
"fmt"
"strconv"
"strings"

"github.com/algorand/avm-abi/abi"
)

// AppCallBytes represents an encoding and a value of an app call argument.
type AppCallBytes struct {
Encoding string `codec:"encoding"`
Value string `codec:"value"`
}

// NewAppCallBytes parses an argument of the form "encoding:value" to AppCallBytes.
func NewAppCallBytes(arg string) (AppCallBytes, error) {
parts := strings.SplitN(arg, ":", 2)
if len(parts) != 2 {
return AppCallBytes{}, fmt.Errorf("all arguments and box names should be of the form 'encoding:value'")
}
return AppCallBytes{
Encoding: parts[0],
Value: parts[1],
}, nil
}

// Raw converts an AppCallBytes arg to a byte array.
func (arg AppCallBytes) Raw() (rawValue []byte, parseErr error) {
switch arg.Encoding {
case "str", "string":
rawValue = []byte(arg.Value)
case "int", "integer":
num, err := strconv.ParseUint(arg.Value, 10, 64)
if err != nil {
parseErr = fmt.Errorf("Could not parse uint64 from string (%s): %v", arg.Value, err)
return
}
ibytes := make([]byte, 8)
binary.BigEndian.PutUint64(ibytes, num)
rawValue = ibytes
case "addr", "address":
addr, err := abi.AddressFromString(arg.Value)
if err != nil {
parseErr = fmt.Errorf("Could not unmarshal checksummed address from string (%s): %v", arg.Value, err)
return
}
rawValue = addr[:]
case "b32", "base32", "byte base32":
data, err := base32.StdEncoding.DecodeString(arg.Value)
if err != nil {
parseErr = fmt.Errorf("Could not decode base32-encoded string (%s): %v", arg.Value, err)
return
}
rawValue = data
case "b64", "base64", "byte base64":
data, err := base64.StdEncoding.DecodeString(arg.Value)
if err != nil {
parseErr = fmt.Errorf("Could not decode base64-encoded string (%s): %v", arg.Value, err)
return
}
rawValue = data
case "abi":
typeAndValue := strings.SplitN(arg.Value, ":", 2)
if len(typeAndValue) != 2 {
parseErr = fmt.Errorf("Could not decode abi string (%s): should split abi-type and abi-value with colon", arg.Value)
return
}
abiType, err := abi.TypeOf(typeAndValue[0])
if err != nil {
parseErr = fmt.Errorf("Could not decode abi type string (%s): %v", typeAndValue[0], err)
return
}
value, err := abiType.UnmarshalFromJSON([]byte(typeAndValue[1]))
if err != nil {
parseErr = fmt.Errorf("Could not decode abi value string (%s):%v ", typeAndValue[1], err)
return
}
return abiType.Encode(value)
default:
parseErr = fmt.Errorf("Unknown encoding: %s", arg.Encoding)
}
return
}
131 changes: 131 additions & 0 deletions apps/parsing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package apps

import (
"encoding/base32"
"encoding/base64"
"encoding/binary"
"fmt"
"math"
"testing"

"github.com/stretchr/testify/require"

"github.com/algorand/avm-abi/abi"
)

func TestNewAppCallBytes(t *testing.T) {
t.Parallel()

t.Run("errors", func(t *testing.T) {
t.Parallel()
_, err := NewAppCallBytes("hello")
require.Error(t, err)

for _, v := range []string{":x", "int:-1"} {
acb, _ := NewAppCallBytes(v)
_, err = acb.Raw()
require.Error(t, err)
}
})

for _, v := range []string{"hello", "1:2"} {
for _, e := range []string{"str", "string"} {
v, e := v, e
t.Run(fmt.Sprintf("encoding=%v,value=%v", e, v), func(t *testing.T) {
t.Parallel()
acb, err := NewAppCallBytes(fmt.Sprintf("%v:%v", e, v))
require.NoError(t, err)
r, err := acb.Raw()
require.NoError(t, err)
require.Equal(t, v, string(r))
})
}

for _, e := range []string{"b32", "base32", "byte base32"} {
ve := base32.StdEncoding.EncodeToString([]byte(v))
e := e
t.Run(fmt.Sprintf("encoding=%v,value=%v", e, ve), func(t *testing.T) {
acb, err := NewAppCallBytes(fmt.Sprintf("%v:%v", e, ve))
require.NoError(t, err)
r, err := acb.Raw()
require.NoError(t, err)
require.Equal(t, ve, base32.StdEncoding.EncodeToString(r))
})
}

for _, e := range []string{"b64", "base64", "byte base64"} {
ve := base64.StdEncoding.EncodeToString([]byte(v))
e := e
t.Run(fmt.Sprintf("encoding=%v,value=%v", e, ve), func(t *testing.T) {
t.Parallel()
acb, err := NewAppCallBytes(fmt.Sprintf("%v:%v", e, ve))
require.NoError(t, err)
r, err := acb.Raw()
require.NoError(t, err)
require.Equal(t, ve, base64.StdEncoding.EncodeToString(r))
})
}
}

for _, v := range []uint64{1, 0, math.MaxUint64} {
for _, e := range []string{"int", "integer"} {
v, e := v, e
t.Run(fmt.Sprintf("encoding=%v,value=%v", e, v), func(t *testing.T) {
t.Parallel()
acb, err := NewAppCallBytes(fmt.Sprintf("%v:%v", e, v))
require.NoError(t, err)
r, err := acb.Raw()
require.NoError(t, err)
require.Equal(t, v, binary.BigEndian.Uint64(r))
})
}
}

for _, v := range []string{"737777777777777777777777777777777777777777777777777UFEJ2CI"} {
for _, e := range []string{"addr", "address"} {
v, e := v, e
t.Run(fmt.Sprintf("encoding=%v,value=%v", e, v), func(t *testing.T) {
t.Parallel()
acb, err := NewAppCallBytes(fmt.Sprintf("%v:%v", e, v))
require.NoError(t, err)
r, err := acb.Raw()
require.NoError(t, err)
addr, err := abi.AddressFromString(v)
require.NoError(t, err)
expectedBytes := addr[:]
require.Equal(t, expectedBytes, r)
})
}
}

type abiCase struct {
abiType, rawValue string
}
for _, v := range []abiCase{
{
`(uint64,string,bool[])`,
`[399,"should pass",[true,false,false,true]]`,
}} {
for _, e := range []string{"abi"} {
v, e := v, e
t.Run(fmt.Sprintf("encoding=%v,value=%v", e, v), func(t *testing.T) {
t.Parallel()
acb, err := NewAppCallBytes(fmt.Sprintf(
"%v:%v:%v", e, v.abiType, v.rawValue))
require.NoError(t, err)
r, err := acb.Raw()
require.NoError(t, err)
require.NotEmpty(t, r)

// Confirm round-trip works.
abiType, err := abi.TypeOf(v.abiType)
require.NoError(t, err)
d, err := abiType.Decode(r)
require.NoError(t, err)
vv, err := abiType.Encode(d)
require.NoError(t, err)
require.Equal(t, r, vv)
})
}
}
}