diff --git a/core/vm/algorand/client.go b/core/vm/algorand/client.go new file mode 100644 index 0000000000..b83575c0c8 --- /dev/null +++ b/core/vm/algorand/client.go @@ -0,0 +1,72 @@ +// Copyright 2023 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package algorand + +import ( + "context" + "fmt" + + "github.com/algorand/go-algorand-sdk/client/v2/algod" + "github.com/algorand/go-algorand-sdk/client/v2/common/models" +) + +// Client is the interface for the Algorand client. +type Client interface { + // GetAccount returns the account information. + GetAccount(address string) (*models.Account, error) + // CheckStatus checks the status of the client. + CheckStatus() error +} + +// AlgorandClient implements the Client interface. +type AlgorandClient struct { + algodAddress string + algodToken string + algodClient *algod.Client +} + +// NewClient creates a new Algorand client. +func NewClient(algodAddress, algodToken string) *AlgorandClient { + algorandClient := &AlgorandClient{ + algodAddress: algodAddress, + algodToken: algodToken, + } + algodClient, err := algod.MakeClient(algodAddress, algodToken) + if err == nil { + algorandClient.algodClient = algodClient + } + return algorandClient +} + +func (c *AlgorandClient) CheckStatus() error { + if c.algodAddress == "" { + return fmt.Errorf("algodAddress is not set") + } + if c.algodToken == "" { + return fmt.Errorf("algodToken is not set") + } + if c.algodClient == nil { + return fmt.Errorf("algodClient is not initialized") + } + return nil +} + +// GetAccount returns the account information. +func (c *AlgorandClient) GetAccount(address string) (*models.Account, error) { + account, err := c.algodClient.AccountInformation(address).Do(context.Background()) + return &account, err +} diff --git a/core/vm/algorand/input.go b/core/vm/algorand/input.go index d47b00af75..c8cb79c87b 100644 --- a/core/vm/algorand/input.go +++ b/core/vm/algorand/input.go @@ -93,6 +93,10 @@ func abiType(name string, typ reflect.Type) string { fields = append(fields, abiType(typ.Field(i).Name, typ.Field(i).Type)) } return strings.Join(fields, ", ") + } else if typ.Kind() == reflect.Int { + return fmt.Sprintf(`{"name": "%s", "type": "int256"}`, name) + } else if typ.Kind() == reflect.Uint { + return fmt.Sprintf(`{"name": "%s", "type": "uint256"}`, name) } else { return fmt.Sprintf(`{"name": "%s", "type": "%s"}`, name, typ.String()) } diff --git a/core/vm/algorand/output.go b/core/vm/algorand/output.go index f8df444677..6102407d6b 100644 --- a/core/vm/algorand/output.go +++ b/core/vm/algorand/output.go @@ -26,12 +26,23 @@ import ( "github.com/ethereum/go-ethereum/log" ) +// pack encodes the value into bytes. func pack(value reflect.Value) ([]byte, error) { + // Convert int to int64 or uint to uint64. + // This is because the ABI does not support int or uint. + // The cast is safe because int/uint is either 32-bit or 64-bit. + if value.Kind() == reflect.Int { + value = reflect.ValueOf(int64(value.Int())) + } else if value.Kind() == reflect.Uint { + value = reflect.ValueOf(uint64(value.Uint())) + } + defn := fmt.Sprintf(`[{"type": "constructor", "inputs": [%s]}]`, abiType("", value.Type())) abi, err := abi.JSON(strings.NewReader(defn)) if err != nil { return nil, err } + data, err := abi.Pack("", value.Interface()) if err != nil { return nil, err diff --git a/core/vm/algorand/output_test.go b/core/vm/algorand/output_test.go new file mode 100644 index 0000000000..66285d0a07 --- /dev/null +++ b/core/vm/algorand/output_test.go @@ -0,0 +1,112 @@ +// Copyright 2023 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package algorand + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/require" +) + +func packValue(v any) (any, error) { + data, err := pack(reflect.ValueOf(v)) + if err != nil { + return nil, err + } + + var vv reflect.Value + switch v.(type) { + case int: + vv = reflect.New(reflect.TypeOf(int64(0))) + case uint: + vv = reflect.New(reflect.TypeOf(uint64(0))) + default: + vv = reflect.New(reflect.TypeOf(v)) + } + err = unpack(data, vv.Interface()) + if err != nil { + return nil, err + } + return vv.Elem().Interface(), nil +} + +func TestPackBool(t *testing.T) { + v, err := packValue(true) + require.NoError(t, err) + require.Equal(t, true, v) + + v, err = packValue(false) + require.NoError(t, err) + require.Equal(t, false, v) +} + +func TestPackString(t *testing.T) { + v, err := packValue("hello") + require.NoError(t, err) + require.Equal(t, "hello", v) +} + +func TestPackByte(t *testing.T) { + v, err := packValue(byte('a')) + require.NoError(t, err) + require.Equal(t, byte('a'), v) +} + +func TestPackInteger(t *testing.T) { + i := 1000000 + + v, err := packValue(int(i)) + require.NoError(t, err) + require.Equal(t, int64(i), v) + + v, err = packValue(int8(i)) + require.NoError(t, err) + require.Equal(t, int8(i), v) + + v, err = packValue(int16(i)) + require.NoError(t, err) + require.Equal(t, int16(i), v) + + v, err = packValue(int32(i)) + require.NoError(t, err) + require.Equal(t, int32(i), v) + + v, err = packValue(int64(i)) + require.NoError(t, err) + require.Equal(t, int64(i), v) + + v, err = packValue(uint(i)) + require.NoError(t, err) + require.Equal(t, uint64(i), v) + + v, err = packValue(uint8(i)) + require.NoError(t, err) + require.Equal(t, uint8(i), v) + + v, err = packValue(uint16(i)) + require.NoError(t, err) + require.Equal(t, uint16(i), v) + + v, err = packValue(uint32(i)) + require.NoError(t, err) + require.Equal(t, uint32(i), v) + + v, err = packValue(uint64(i)) + require.NoError(t, err) + require.Equal(t, uint64(i), v) +} diff --git a/core/vm/algorand/precompiled.go b/core/vm/algorand/precompiled.go index 50ed73ede2..b65950a405 100644 --- a/core/vm/algorand/precompiled.go +++ b/core/vm/algorand/precompiled.go @@ -17,34 +17,29 @@ package algorand import ( - "context" "fmt" "os" "reflect" + "strings" - "github.com/algorand/go-algorand-sdk/client/v2/algod" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" ) // Algorand implements the precompile to access the Algorand blockchain. type Algorand struct { - algodAddress string - algodToken string - algodClient *algod.Client + algodClient Client } // New creates a new Algorand precompiled contract with a client to access Algorand blockchain. func New() *Algorand { algodAddress := os.Getenv("ALGOD_ADDRESS") algodToken := os.Getenv("ALGOD_TOKEN") - algorand := &Algorand{ - algodAddress: algodAddress, - algodToken: algodToken, + if !strings.HasPrefix(algodAddress, "http://") { + algodAddress = "http://" + algodAddress } - algodClient, err := algod.MakeClient(algodAddress, algodToken) - if err == nil { - algorand.algodClient = algodClient + algorand := &Algorand{ + algodClient: NewClient(algodAddress, algodToken), } return algorand } @@ -56,8 +51,9 @@ func (a *Algorand) RequiredGas(input []byte) uint64 { // Run executes the Algorand precompile with the given input. func (a *Algorand) Run(input []byte) ([]byte, error) { - if a.algodClient == nil { - return nil, fmt.Errorf("cannot connect to Algorand node") + err := a.algodClient.CheckStatus() + if err != nil { + return nil, err } params, err := UnpackInput(input) @@ -67,13 +63,13 @@ func (a *Algorand) Run(input []byte) ([]byte, error) { var info interface{} switch params.GetCmdType() { case AccountCmd: - info, err = a.algodClient.AccountInformation(params.(*AccountInput).Address).Do(context.Background()) + info, err = a.algodClient.GetAccount(params.(*AccountInput).Address) if err != nil { return nil, err } } log.Info("Algorand.Run", "info", info) - value := reflect.ValueOf(info).FieldByName(params.GetFieldName()) + value := reflect.ValueOf(info).Elem().FieldByName(params.GetFieldName()) if !value.IsValid() { return nil, fmt.Errorf("field %s does not exist", params.GetFieldName()) } diff --git a/core/vm/algorand/precompiled_test.go b/core/vm/algorand/precompiled_test.go new file mode 100644 index 0000000000..3d6636b2d3 --- /dev/null +++ b/core/vm/algorand/precompiled_test.go @@ -0,0 +1,73 @@ +// Copyright 2023 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package algorand + +import ( + "fmt" + "testing" + + "github.com/algorand/go-algorand-sdk/client/v2/common/models" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +// MockClient is a mock client for testing. +type MockClient struct { + // store is mapping from Algorand addresses to Algorand accounts. + store map[string]*models.Account +} + +// NewMockClient creates a new mock client. +func NewMockClient() *MockClient { + store := make(map[string]*models.Account) + store["737777777777777777777777777777777777777777777777777UFEJ2CI"] = &models.Account{ + Address: "737777777777777777777777777777777777777777777777777UFEJ2CI", + Amount: 10000000, + } + return &MockClient{ + store: store, + } +} + +// GetAccount returns the account information. +func (c *MockClient) GetAccount(address string) (*models.Account, error) { + if _, ok := c.store[address]; !ok { + return nil, fmt.Errorf("account not found") + } + return c.store[address], nil +} + +// CheckStatus of MockClient is always successful. +func (c *MockClient) CheckStatus() error { + return nil +} + +// TestRun tests the Run function on an Algorand object with a MockClient. +func TestRun(t *testing.T) { + algorand := &Algorand{ + algodClient: NewMockClient(), + } + rawInput, err := common.ParseHexOrString("0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000006416d6f756e740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003a3733373737373737373737373737373737373737373737373737373737373737373737373737373737373737373737373737375546454a324349000000000000") + require.NoError(t, err) + + rawOutput, err := algorand.Run(rawInput) + require.NoError(t, err) + v := new(uint64) + err = unpack(rawOutput, v) + require.NoError(t, err) + require.Equal(t, uint64(10000000), *v) +} diff --git a/core/vm/contracts.go b/core/vm/contracts.go index a7707c6a52..1b61877ad1 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -31,6 +31,7 @@ import ( "github.com/ethereum/go-ethereum/crypto/bls12381" "github.com/ethereum/go-ethereum/crypto/bn256" "github.com/ethereum/go-ethereum/crypto/kzg4844" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "golang.org/x/crypto/ripemd160" ) @@ -177,6 +178,7 @@ func RunPrecompiledContract(p PrecompiledContract, input []byte, suppliedGas uin } suppliedGas -= gasCost output, err := p.Run(input) + log.Info("RunPrecompiledContract", "err", err) return output, suppliedGas, err }