Skip to content

Commit

Permalink
Support arbitrary contract calls (#224)
Browse files Browse the repository at this point in the history
* Support arbitrary contract calls (#223)

* support generic contract calls

* support test on arm64

* address pr comments

* address pr comments

* panic if type conversion fails

* address pr comments

* fix

* fix checking generic contract call method

* fix docker build

* define consts at top of file

* cleanup construction checks

* add link to type source

* fix linting errors

* add comment explaining how generic check works

* add more comments

---------

Co-authored-by: cryptoriver <114266151+cryptoriver@users.noreply.github.com>
  • Loading branch information
patrick-ogrady and cryptoriver authored Oct 23, 2023
1 parent a0889a6 commit b847524
Show file tree
Hide file tree
Showing 9 changed files with 807 additions and 33 deletions.
71 changes: 71 additions & 0 deletions Dockerfile.arm64
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# ------------------------------------------------------------------------------
# Build avalanche
# ------------------------------------------------------------------------------
FROM arm64v8/golang:1.20.10-bullseye AS avalanche

ARG AVALANCHE_VERSION

RUN git clone https://github.com/ava-labs/avalanchego.git \
/go/src/github.com/ava-labs/avalanchego

WORKDIR /go/src/github.com/ava-labs/avalanchego

RUN git checkout $AVALANCHE_VERSION && \
./scripts/build.sh

# ------------------------------------------------------------------------------
# Build avalanche rosetta
# ------------------------------------------------------------------------------
FROM arm64v8/golang:1.20.10-bullseye AS rosetta

ARG ROSETTA_VERSION

RUN git clone https://github.com/ava-labs/avalanche-rosetta.git \
/go/src/github.com/ava-labs/avalanche-rosetta

WORKDIR /go/src/github.com/ava-labs/avalanche-rosetta

RUN git checkout $ROSETTA_VERSION && \
go mod download

RUN \
GO_VERSION=$(go version | awk {'print $3'}) \
GIT_COMMIT=$(git rev-parse HEAD) \
make build

# ------------------------------------------------------------------------------
# Target container for running the node and rosetta server
# ------------------------------------------------------------------------------
FROM arm64v8/ubuntu:20.04

# Install dependencies
RUN apt-get update -y && \
apt-get install -y wget

WORKDIR /app

# Install avalanche daemon
COPY --from=avalanche \
/go/src/github.com/ava-labs/avalanchego/build/avalanchego \
/app/avalanchego

# Install rosetta server
COPY --from=rosetta \
/go/src/github.com/ava-labs/avalanche-rosetta/rosetta-server \
/app/rosetta-server

# Install rosetta runner
COPY --from=rosetta \
/go/src/github.com/ava-labs/avalanche-rosetta/rosetta-runner \
/app/rosetta-runner

# Install service start script
COPY --from=rosetta \
/go/src/github.com/ava-labs/avalanche-rosetta/docker/entrypoint.sh \
/app/entrypoint.sh

EXPOSE 9650
EXPOSE 9651
EXPOSE 8080

ENTRYPOINT ["/app/entrypoint.sh"]
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ docker-build:
-f Dockerfile \
.

docker-build-arm64:
docker build \
--build-arg AVALANCHE_VERSION=${AVALANCHE_VERSION} \
--build-arg ROSETTA_VERSION=${GIT_COMMIT} \
-t ${DOCKER_TAG} \
-f Dockerfile.arm64 \
.

# Start the Testnet in ONLINE mode
run-testnet:
docker run \
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ Before we can start the service, we need to build the docker image:
make docker-build
```

**Note:** If you're running arm64 machine(e.g, Mac M1), you may run

```bash
make docker-build-arm64
```

Next, start the Testnet service by running:

```bash
Expand Down
2 changes: 1 addition & 1 deletion service/backend/pchain/construction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ func TestImportTxConstruction(t *testing.T) {

unsignedImportTx := "0x000000000011000000050000000000000000000000000000000000000000000000000000000000000000000000013d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa00000007000000003b8b87c0000000000000000000000001000000015445cd01d75b4a06b6b41939193c0b1c5544490d00000000000000007fc93d85c6d62c5b2ac0b519c87010ea5294012d1e407030d6acd0021cac10d500000001f52a5a6dd8f1b3fe05204bdab4f6bcb5a7059f88d0443c636f6c158f838dd1a8000000003d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa00000005000000003b9aca000000000100000000000000004ce8b27d"
unsignedImportTxHash, _ := hex.DecodeString("e9114ae12065d1f8631bc40729c806a3a4793de714001bfee66482f520dc1865")
wrappedUnsignedImportTx := `{"tx":"` + unsignedImportTx + `","signers":` + importSigners + `}`
wrappedUnsignedImportTx := `{"tx":"` + unsignedImportTx + `","signers":` + importSigners + `}` //nolint:goconst

signingPayloads := []*types.SigningPayload{
{
Expand Down
181 changes: 181 additions & 0 deletions service/contract_call_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package service

import (
"encoding/hex"
"errors"
"fmt"
"log"
"math/big"
"strconv"
"strings"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
)

// The following implementations are derived from rosetta-geth-sdk:
//
// https://github.com/coinbase/rosetta-geth-sdk/blob/master/services/construction/contract_call_data.go

const (
split = 2
base10 = 10
)

// constructContractCallDataGeneric constructs the data field of a transaction.
// The methodArgs can be already in ABI encoded format in case of a single string
// It can also be passed in as a slice of args, which requires further encoding.
func constructContractCallDataGeneric(methodSig string, methodArgs interface{}) ([]byte, error) {
data, err := contractCallMethodID(methodSig)
if err != nil {
return nil, err
}

// switch on the type of the method args. method args can come in from json as either a string or list of strings
switch methodArgs := methodArgs.(type) {
// case 0: no method arguments, return the selector
case nil:
return data, nil

// case 1: method args are pre-compiled ABI data. decode the hex and create the call data directly
case string:
methodArgs = strings.TrimPrefix(methodArgs, "0x")
b, decErr := hex.DecodeString(methodArgs)
if decErr != nil {
return nil, fmt.Errorf("error decoding method args hex data: %w", decErr)
}
return append(data, b...), nil

// case 2: method args are a list of interface{} which will be converted to string before encoding
case []interface{}:
var strList []string
for i, genericVal := range methodArgs {
strVal, isStrVal := genericVal.(string)
if !isStrVal {
return nil, fmt.Errorf("invalid method_args type at index %d: %T (must be a string)",
i, genericVal,
)
}
strList = append(strList, strVal)
}
return encodeMethodArgsStrings(data, methodSig, strList)

// case 3: method args are encoded as a list of strings, which will be decoded
case []string:
return encodeMethodArgsStrings(data, methodSig, methodArgs)

// case 4: there is no known way to decode the method args
default:
return nil, fmt.Errorf(
"invalid method_args type, accepted values are []string and hex-encoded string."+
" type received=%T value=%#v", methodArgs, methodArgs,
)
}
}

// encodeMethodArgsStrings constructs the data field of a transaction for a list of string args.
// It attempts to first convert the string arg to it's corresponding type in the method signature,
// and then performs abi encoding to the converted args list and construct the data.
func encodeMethodArgsStrings(methodID []byte, methodSig string, methodArgs []string) ([]byte, error) {
var data []byte
data = append(data, methodID...)

splitSigByLeadingParenthesis := strings.Split(methodSig, "(")
if len(splitSigByLeadingParenthesis) < split {
return data, nil
}
splitSigByTrailingParenthesis := strings.Split(splitSigByLeadingParenthesis[1], ")")
if len(splitSigByTrailingParenthesis) < 1 {
return data, nil
}
splitSigByComma := strings.Split(splitSigByTrailingParenthesis[0], ",")
if len(splitSigByComma) != len(methodArgs) {
return nil, errors.New("invalid method arguments")
}

arguments := abi.Arguments{}
argumentsData := make([]interface{}, 0, len(splitSigByComma))
for i, v := range splitSigByComma {
typed, _ := abi.NewType(v, v, nil)
argument := abi.Arguments{
{
Type: typed,
},
}

arguments = append(arguments, argument...)
var argData interface{}
switch {
case v == "address":
{
argData = common.HexToAddress(methodArgs[i])
}
case v == "uint32":
{
u64, err := strconv.ParseUint(methodArgs[i], base10, 32)
if err != nil {
log.Fatal(err)
}
argData = uint32(u64)
}
case strings.HasPrefix(v, "uint") || strings.HasPrefix(v, "int"):
{
value := new(big.Int)
value.SetString(methodArgs[i], base10)
argData = value
}
case v == "bytes32":
{
value := [32]byte{}
bytes, err := hexutil.Decode(methodArgs[i])
if err != nil {
log.Fatal(err)
}
copy(value[:], bytes)
argData = value
}
case strings.HasPrefix(v, "bytes"):
{
bytes, err := hexutil.Decode(methodArgs[i])
if err != nil {
log.Fatal(err)
}
argData = bytes
}
case strings.HasPrefix(v, "string"):
{
argData = methodArgs[i]
}
case strings.HasPrefix(v, "bool"):
{
value, err := strconv.ParseBool(methodArgs[i])
if err != nil {
log.Fatal(err)
}
argData = value
}
default:
return nil, fmt.Errorf("invalid argument type: %s", v)
}
argumentsData = append(argumentsData, argData)
}

abiEncodeData, err := arguments.PackValues(argumentsData)
if err != nil {
return nil, fmt.Errorf("failed to encode arguments: %w", err)
}

data = append(data, abiEncodeData...)
return data, nil
}

// contractCallMethodID calculates the first 4 bytes of the method
// signature for function call on contract
func contractCallMethodID(methodSig string) ([]byte, error) {
if len(methodSig) < 4 {
return nil, fmt.Errorf("method signature is empty or too small")
}
return crypto.Keccak256([]byte(methodSig))[:4], nil
}
68 changes: 68 additions & 0 deletions service/contract_call_data_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package service

import (
"errors"
"fmt"
"testing"

"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/stretchr/testify/assert"
)

func TestConstruction_ContractCallData(t *testing.T) {
tests := map[string]struct {
methodSig string
methodArgs interface{}

expectedResponse string
expectedError error
}{
"happy path: nil args": {
methodSig: "deposit()",
methodArgs: nil,
expectedResponse: "0xd0e30db0",
},
"happy path: single string arg": {
methodSig: "attest((bytes32,(address,uint64,bool,bytes32,bytes,uint256)))",
methodArgs: "0x00000000000000000000000000000000000000000000000000000000000000201cdb5651ea836ecc9be70d044e2cf7a416e5257ec8d954deb9d09a66a8264b8e000000000000000000000000000000000000000000000000000000000000004000000000000000000000000026c58c5095c8fac99e518ee951ba8f56d3c75e8e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001",
expectedResponse: "0xf17325e700000000000000000000000000000000000000000000000000000000000000201cdb5651ea836ecc9be70d044e2cf7a416e5257ec8d954deb9d09a66a8264b8e000000000000000000000000000000000000000000000000000000000000004000000000000000000000000026c58c5095c8fac99e518ee951ba8f56d3c75e8e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001",
},
"happy path: list of string args": {
methodSig: "register(string,address,bool)",
methodArgs: []string{"bool abc", "0x0000000000000000000000000000000000000000", "true"},
expectedResponse: "0x60d7a2780000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000008626f6f6c20616263000000000000000000000000000000000000000000000000",
},
"happy path: list of non string args": {
methodSig: "register(string,address,bool)",
methodArgs: []interface{}{"bool abc", "0x0000000000000000000000000000000000000000", "true"},
expectedResponse: "0x60d7a2780000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000008626f6f6c20616263000000000000000000000000000000000000000000000000",
},
"error: case string: invalid method args hex data": {
methodSig: "attest((bytes32,(address,uint64,bool,bytes32,bytes,uint256)))",
methodArgs: "!!!",
expectedError: errors.New("error decoding method args hex data: encoding/hex: invalid byte: U+0021 '!'"),
},
"error: case []interface: ": {
methodSig: "register(string,address,bool)",
methodArgs: []interface{}{"bool abc", "0x0000000000000000000000000000000000000000", true},
expectedError: errors.New("invalid method_args type at index 2: bool (must be a string)"),
},
"error: bad argument type": {
methodSig: "attest(bytes32,foo)",
methodArgs: []string{"0x0000000000000000000000000000000000000000000000000000000000000000", "bar"},
expectedError: fmt.Errorf("invalid argument type: %s", "foo"),
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
bytes, err := constructContractCallDataGeneric(test.methodSig, test.methodArgs)
if err != nil {
fmt.Println(err)
assert.EqualError(t, err, test.expectedError.Error())
} else {
assert.Equal(t, test.expectedResponse, hexutil.Encode(bytes))
}
})
}
}
Loading

0 comments on commit b847524

Please sign in to comment.