Skip to content

Commit

Permalink
Merge pull request #16 from cryptoriver/contract_call_support
Browse files Browse the repository at this point in the history
Enable arbitrary contract call support
  • Loading branch information
mboben authored Oct 12, 2023
2 parents d4161f6 + d7f6ef0 commit 3d5ee33
Show file tree
Hide file tree
Showing 7 changed files with 737 additions and 27 deletions.
115 changes: 115 additions & 0 deletions server/Dockerfile.arm64
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# ------------------------------------------------------------------------------
# Build go-flare
# ------------------------------------------------------------------------------
FROM arm64v8/golang:1.18.5 AS flare

WORKDIR /app

ARG GO_FLARE_VERSION=v1.7.1806
ARG GO_FLARE_REPO=https://github.com/flare-foundation/go-flare

RUN git clone --branch "$GO_FLARE_VERSION" "${GO_FLARE_REPO}" .

RUN apt update && \
apt -y install rsync
RUN cd avalanchego && \
./scripts/build.sh

# ------------------------------------------------------------------------------
# Build flare-rosetta
# ------------------------------------------------------------------------------
FROM arm64v8/golang:1.18.5 AS rosetta

ARG ROSETTA_SRC=https://github.com/flare-foundation/flare-rosetta/archive/refs/heads/main.zip
ARG ROSETTA_SRC_ZIP_SUBFOLDER=flare-rosetta-main

WORKDIR /tmp

RUN apt update -y && apt install unzip -y
ADD ${ROSETTA_SRC} /tmp/rosetta-source
RUN if [ ! -d "/tmp/rosetta-source" ]; then \
unzip "/tmp/rosetta-source" && \
mv /tmp/${ROSETTA_SRC_ZIP_SUBFOLDER} /app; \
else \
mv /tmp/rosetta-source /app; \
fi

WORKDIR /app/server

RUN \
GO_VERSION=$(go version | awk {'print $3'}) \
GIT_COMMIT=main \
go mod download && \
make setup && \
make build

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

ENV DEBIAN_FRONTEND=noninteractive

# Install dependencies
RUN apt-get update -y && \
apt -y upgrade && \
apt -y install curl jq netcat dnsutils moreutils

WORKDIR /app

ENV HTTP_HOST=0.0.0.0 \
HTTP_PORT=9650 \
STAKING_PORT=9651 \
DB_DIR=/data \
DB_TYPE=leveldb \
LOG_DIR=/app/flare/logs \
MODE=online \
START_ROSETTA_SERVER_AFTER_BOOTSTRAP=false \
AUTOCONFIGURE_BOOTSTRAP_ENDPOINT="" \
AUTOCONFIGURE_BOOTSTRAP_ENDPOINT_RETRY=0 \
STAKING_ENABLED="true" \
YES_I_REALLY_KNOW_WHAT_I_AM_DOING="false"



# Intentionally empty, so env vars are evalueted at runtime (script entrypoint_flare.sh) instead of build time
# Default: /app/conf/$NETWORK_ID
ENV CHAIN_CONFIG_DIR=""
# Default: warn
ENV LOG_LEVEL=""

# ROSETTA ENV VARS
ENV ROSETTA_FLARE_ENDPOINT=http://127.0.0.1:9650
# Intentionally empty, so env vars are evalueted at runtime (script entrypoint_rosetta.sh) instead of build time
# Default: /app/conf/$NETWORK_ID/server-config.json
ENV ROSETTA_CONFIG_PATH=""

# Install flare
COPY --from=flare /app/avalanchego/build /app/flare/build

# Install rosetta server
COPY --from=rosetta /app/server /app/rosetta-server

# Install node and rosetta configs
COPY --from=rosetta /app/server/rosetta-cli-conf /app/conf

# Install entrypoints
COPY --from=rosetta /app/server/docker/entrypoint_flare.sh /app/entrypoint_flare.sh
COPY --from=rosetta /app/server/docker/entrypoint_rosetta.sh /app/entrypoint_rosetta.sh
COPY --from=rosetta /app/server/docker/entrypoint_main.sh /app/entrypoint_main.sh
COPY --from=rosetta /app/server/docker/healthcheck.sh /app/healthcheck.sh

# Copy testnet configs
COPY --from=flare /app/avalanchego/staking/local /app/flare/staking/local
COPY --from=flare /app/config/localflare /app/flare/config/localflare

RUN chmod +x entrypoint_flare.sh entrypoint_rosetta.sh entrypoint_main.sh

EXPOSE ${HTTP_PORT}
EXPOSE ${STAKING_PORT}
EXPOSE 8080

HEALTHCHECK --interval=5s --timeout=5s --retries=5 --start-period=15s CMD bash /app/healthcheck.sh

ENTRYPOINT ["/bin/bash"]
CMD ["/app/entrypoint_main.sh"]
8 changes: 8 additions & 0 deletions server/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
184 changes: 184 additions & 0 deletions server/service/contract_call_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
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"
"golang.org/x/crypto/sha3"
)

// 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) {
arguments := abi.Arguments{}
var argumentsData []interface{}

var data []byte
data = append(data, methodID...)

const split = 2
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")
}

for i, v := range splitSigByComma {
typed, _ := abi.NewType(v, v, nil)
argument := abi.Arguments{
{
Type: typed,
},
}

arguments = append(arguments, argument...)
var argData interface{}
const base = 10
switch {
case v == "address":
{
argData = common.HexToAddress(methodArgs[i])
}
case v == "uint32":
{
u64, err := strconv.ParseUint(methodArgs[i], 10, 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], base)
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"):
{
// No fixed size set as it would make it an "array" instead
// of a "slice" when encoding. We want it to be a slice.
value := []byte{}
bytes, err := hexutil.Decode(methodArgs[i])
if err != nil {
log.Fatal(err)
}
copy(value[:], bytes) // nolint:gocritic
argData = value
}
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, errors.New(fmt.Sprintf("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) {
fnSignature := []byte(methodSig)
hash := sha3.NewLegacyKeccak256()
if _, err := hash.Write(fnSignature); err != nil {
return nil, err
}

return hash.Sum(nil)[:4], nil
}
63 changes: 63 additions & 0 deletions server/service/contract_call_data_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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)"),
},
}

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 3d5ee33

Please sign in to comment.