From cf744c9eeed3c43b99976a4d8e4c067f900cf4d7 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Fri, 12 Aug 2022 09:04:58 -0400 Subject: [PATCH 1/2] Splitting out deploy and adding coverage Signed-off-by: Peter Broadhurst --- internal/ethereum/deploy_contract_prepare.go | 115 ++++++++++++++++++ .../ethereum/deploy_contract_prepare_test.go | 94 ++++++++++++++ internal/ethereum/event_stream_test.go | 5 +- internal/ethereum/prepare_transaction.go | 97 ++------------- 4 files changed, 220 insertions(+), 91 deletions(-) create mode 100644 internal/ethereum/deploy_contract_prepare.go create mode 100644 internal/ethereum/deploy_contract_prepare_test.go diff --git a/internal/ethereum/deploy_contract_prepare.go b/internal/ethereum/deploy_contract_prepare.go new file mode 100644 index 0000000..dea9766 --- /dev/null +++ b/internal/ethereum/deploy_contract_prepare.go @@ -0,0 +1,115 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ethereum + +import ( + "context" + "encoding/base64" + "encoding/hex" + "encoding/json" + "strings" + + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-evmconnect/internal/msgs" + "github.com/hyperledger/firefly-signer/pkg/abi" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +func (c *ethConnector) DeployContractPrepare(ctx context.Context, req *ffcapi.ContractDeployPrepareRequest) (res *ffcapi.TransactionPrepareResponse, reason ffcapi.ErrorReason, err error) { + + // Parse the input JSON data, to build the call data + callData, method, err := c.prepareDeployData(ctx, req) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + + // Build the base transaction object + tx, err := c.buildTx(ctx, txTypeDeployContract, req.From, "", req.Nonce, req.Gas, req.Value, callData) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + + if req.Gas, reason, err = c.ensureGasEstimate(ctx, tx, method, req.Gas); err != nil { + return nil, reason, err + } + log.L(ctx).Infof("Prepared transaction method=%s dataLen=%d gas=%s", method.String(), len(callData), req.Gas.Int()) + + return &ffcapi.TransactionPrepareResponse{ + Gas: req.Gas, + TransactionData: ethtypes.HexBytes0xPrefix(callData).String(), + }, "", nil + +} + +func (c *ethConnector) prepareDeployData(ctx context.Context, req *ffcapi.ContractDeployPrepareRequest) ([]byte, *abi.Entry, error) { + // Parse the bytecode as a hex string, or fallback to Base64 + var bytecodeString string + if err := req.Contract.Unmarshal(ctx, &bytecodeString); err != nil { + return nil, nil, i18n.NewError(ctx, msgs.MsgDecodeBytecodeFailed) + } + bytecode, err := hex.DecodeString(strings.TrimPrefix(bytecodeString, "0x")) + if err != nil { + bytecode, err = base64.StdEncoding.DecodeString(bytecodeString) + if err != nil { + return nil, nil, i18n.NewError(ctx, msgs.MsgDecodeBytecodeFailed) + } + } + + // Parse the ABI + var a *abi.ABI + err = json.Unmarshal(req.Definition.Bytes(), &a) + if err != nil { + return nil, nil, i18n.NewError(ctx, msgs.MsgUnmarshalABIFail, err) + } + + // Find the constructor in the ABI + method := a.Constructor() + if method == nil { + // Constructors are optional, so if there is none, simply return the bytecode as the calldata + return bytecode, nil, nil + } + + // Parse the params into the standard semantics of Go JSON unmarshalling, with []interface{} + ethParams := make([]interface{}, len(req.Params)) + for i, p := range req.Params { + if p != nil { + err := json.Unmarshal([]byte(*p), ðParams[i]) + if err != nil { + return nil, nil, i18n.NewError(ctx, msgs.MsgUnmarshalParamFail, i, err) + } + } + } + + // Match the parameters to the ABI call data for the method. + // Note the FireFly ABI decoding package handles formatting errors / translation etc. + var callData []byte + paramValues, err := method.Inputs.ParseExternalDataCtx(ctx, ethParams) + if err != nil { + return nil, nil, err + } + callData, err = paramValues.EncodeABIData() + if err != nil { + return nil, nil, err + } + + // Concatenate bytecode and constructor args for deployment transaction + callData = append(bytecode, callData...) + + return callData, method, err +} diff --git a/internal/ethereum/deploy_contract_prepare_test.go b/internal/ethereum/deploy_contract_prepare_test.go new file mode 100644 index 0000000..a788633 --- /dev/null +++ b/internal/ethereum/deploy_contract_prepare_test.go @@ -0,0 +1,94 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ethereum + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/hyperledger/firefly-signer/pkg/ethtypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +const samplePrepareDeployTX = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "DeployContract" + }, + "from": "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", + "to": "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", + "gas": 1000000, + "nonce": "111", + "value": "12345678901234567890123456789", + "contract": "0xfeedbeef", + "definition": [{ + "inputs": [ + { + "internalType":" uint256", + "name": "x", + "type": "uint256" + } + ], + "outputs":[], + "type":"constructor" + }], + "params": [ 4276993775 ] +}` + +func TestDeployContractPrepareOkNoEstimate(t *testing.T) { + + ctx, c, _, done := newTestConnector(t) + defer done() + + var req ffcapi.ContractDeployPrepareRequest + err := json.Unmarshal([]byte(samplePrepareDeployTX), &req) + assert.NoError(t, err) + res, reason, err := c.DeployContractPrepare(ctx, &req) + + assert.NoError(t, err) + assert.Empty(t, reason) + + assert.Equal(t, int64(1000000), res.Gas.Int64()) + +} + +func TestDeployContractPrepareWithEstimateRevert(t *testing.T) { + + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_estimateGas", mock.Anything).Return(fmt.Errorf("pop")) + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_call", mock.Anything, "latest").Run( + func(args mock.Arguments) { + *(args[1].(*ethtypes.HexBytes0xPrefix)) = ethtypes.MustNewHexBytes0xPrefix("0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000114d75707065747279206465746563746564000000000000000000000000000000") + }, + ).Return(nil) + + var req ffcapi.ContractDeployPrepareRequest + err := json.Unmarshal([]byte(samplePrepareDeployTX), &req) + assert.NoError(t, err) + req.Gas = nil + res, reason, err := c.DeployContractPrepare(ctx, &req) + assert.Regexp(t, "FF23021", err) + assert.Equal(t, ffcapi.ErrorReasonTransactionReverted, reason) + assert.Nil(t, res) + +} diff --git a/internal/ethereum/event_stream_test.go b/internal/ethereum/event_stream_test.go index 125ec59..416fde0 100644 --- a/internal/ethereum/event_stream_test.go +++ b/internal/ethereum/event_stream_test.go @@ -273,7 +273,10 @@ func TestCatchupThenRejoinLeadGroup(t *testing.T) { // Confirm the listener joins the group started := time.Now() for { - assert.True(t, time.Since(started) < 5*time.Second) + t.Logf("Catchup=%t HeadBlock=%d", l.catchup, es.headBlock) + if time.Since(started) > 1*time.Second { + assert.Fail(t, "Never exited catchup") + } if l.catchup { time.Sleep(1 * time.Microsecond) continue diff --git a/internal/ethereum/prepare_transaction.go b/internal/ethereum/prepare_transaction.go index 48ac308..ade6b80 100644 --- a/internal/ethereum/prepare_transaction.go +++ b/internal/ethereum/prepare_transaction.go @@ -18,11 +18,8 @@ package ethereum import ( "context" - "encoding/base64" - "encoding/hex" "encoding/json" "fmt" - "strings" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" @@ -43,7 +40,7 @@ const ( txTypePrePrepared ) -func (c *ethConnector) TransactionPrepare(ctx context.Context, req *ffcapi.TransactionPrepareRequest) (*ffcapi.TransactionPrepareResponse, ffcapi.ErrorReason, error) { +func (c *ethConnector) TransactionPrepare(ctx context.Context, req *ffcapi.TransactionPrepareRequest) (res *ffcapi.TransactionPrepareResponse, reason ffcapi.ErrorReason, err error) { // Parse the input JSON data, to build the call data callData, method, err := c.prepareCallData(ctx, &req.TransactionInput) @@ -57,13 +54,8 @@ func (c *ethConnector) TransactionPrepare(ctx context.Context, req *ffcapi.Trans return nil, ffcapi.ErrorReasonInvalidInputs, err } - if req.Gas == nil || req.Gas.Int().Sign() == 0 { - // If a value for gas has not been supplied, do a gas estimate - gas, reason, err := c.estimateGas(ctx, tx, method) - if err != nil { - return nil, reason, err - } - req.Gas = (*fftypes.FFBigInt)(gas) + if req.Gas, reason, err = c.ensureGasEstimate(ctx, tx, method, req.Gas); err != nil { + return nil, reason, err } log.L(ctx).Infof("Prepared transaction method=%s dataLen=%d gas=%s", method.String(), len(callData), req.Gas.Int()) @@ -74,34 +66,16 @@ func (c *ethConnector) TransactionPrepare(ctx context.Context, req *ffcapi.Trans } -func (c *ethConnector) DeployContractPrepare(ctx context.Context, req *ffcapi.ContractDeployPrepareRequest) (*ffcapi.TransactionPrepareResponse, ffcapi.ErrorReason, error) { - - // Parse the input JSON data, to build the call data - callData, method, err := c.prepareDeployData(ctx, req) - if err != nil { - return nil, ffcapi.ErrorReasonInvalidInputs, err - } - - // Build the base transaction object - tx, err := c.buildTx(ctx, txTypeDeployContract, req.From, "", req.Nonce, req.Gas, req.Value, callData) - if err != nil { - return nil, ffcapi.ErrorReasonInvalidInputs, err - } - - if req.Gas == nil || req.Gas.Int().Sign() == 0 { +func (c *ethConnector) ensureGasEstimate(ctx context.Context, tx *ethsigner.Transaction, method *abi.Entry, gasRequest *fftypes.FFBigInt) (*fftypes.FFBigInt, ffcapi.ErrorReason, error) { + if gasRequest == nil || gasRequest.Int().Sign() == 0 { // If a value for gas has not been supplied, do a gas estimate gas, reason, err := c.estimateGas(ctx, tx, method) if err != nil { return nil, reason, err } - req.Gas = (*fftypes.FFBigInt)(gas) + gasRequest = (*fftypes.FFBigInt)(gas) } - - return &ffcapi.TransactionPrepareResponse{ - Gas: req.Gas, - TransactionData: ethtypes.HexBytes0xPrefix(callData).String(), - }, "", nil - + return gasRequest, ffcapi.ErrorReason(""), nil } func (c *ethConnector) prepareCallData(ctx context.Context, req *ffcapi.TransactionInput) ([]byte, *abi.Entry, error) { @@ -138,63 +112,6 @@ func (c *ethConnector) prepareCallData(ctx context.Context, req *ffcapi.Transact return callData, method, err } -func (c *ethConnector) prepareDeployData(ctx context.Context, req *ffcapi.ContractDeployPrepareRequest) ([]byte, *abi.Entry, error) { - // Parse the bytecode as a hex string, or fallback to Base64 - var bytecodeString string - if err := req.Contract.Unmarshal(ctx, &bytecodeString); err != nil { - return nil, nil, i18n.NewError(ctx, msgs.MsgDecodeBytecodeFailed) - } - bytecode, err := hex.DecodeString(strings.TrimPrefix(bytecodeString, "0x")) - if err != nil { - bytecode, err = base64.StdEncoding.DecodeString(bytecodeString) - if err != nil { - return nil, nil, i18n.NewError(ctx, msgs.MsgDecodeBytecodeFailed) - } - } - - // Parse the ABI - var a *abi.ABI - err = json.Unmarshal(req.Definition.Bytes(), &a) - if err != nil { - return nil, nil, i18n.NewError(ctx, msgs.MsgUnmarshalABIFail, err) - } - - // Find the constructor in the ABI - method := a.Constructor() - if method == nil { - // Constructors are optional, so if there is none, simply return the bytecode as the calldata - return bytecode, nil, nil - } - - // Parse the params into the standard semantics of Go JSON unmarshalling, with []interface{} - ethParams := make([]interface{}, len(req.Params)) - for i, p := range req.Params { - if p != nil { - err := json.Unmarshal([]byte(*p), ðParams[i]) - if err != nil { - return nil, nil, i18n.NewError(ctx, msgs.MsgUnmarshalParamFail, i, err) - } - } - } - - // Match the parameters to the ABI call data for the method. - // Note the FireFly ABI decoding package handles formatting errors / translation etc. - var callData []byte - paramValues, err := method.Inputs.ParseExternalDataCtx(ctx, ethParams) - if err != nil { - return nil, nil, err - } - callData, err = paramValues.EncodeABIData() - if err != nil { - return nil, nil, err - } - - // Concatenate bytecode and constructor args for deployment transaction - callData = append(bytecode, callData...) - - return callData, method, err -} - func (c *ethConnector) buildTx(ctx context.Context, txType txType, fromString, toString string, nonce, gas, value *fftypes.FFBigInt, data []byte) (tx *ethsigner.Transaction, err error) { tx = ðsigner.Transaction{ Nonce: (*ethtypes.HexInteger)(nonce), From dc1c7bb2751413c5957415b28448992ad5ff4123 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Fri, 12 Aug 2022 11:31:25 -0400 Subject: [PATCH 2/2] Add deploy unit tests Signed-off-by: Peter Broadhurst --- internal/ethereum/deploy_contract_prepare.go | 11 +- .../ethereum/deploy_contract_prepare_test.go | 146 ++++++++++++++++++ 2 files changed, 151 insertions(+), 6 deletions(-) diff --git a/internal/ethereum/deploy_contract_prepare.go b/internal/ethereum/deploy_contract_prepare.go index dea9766..34864d7 100644 --- a/internal/ethereum/deploy_contract_prepare.go +++ b/internal/ethereum/deploy_contract_prepare.go @@ -34,7 +34,7 @@ import ( func (c *ethConnector) DeployContractPrepare(ctx context.Context, req *ffcapi.ContractDeployPrepareRequest) (res *ffcapi.TransactionPrepareResponse, reason ffcapi.ErrorReason, err error) { // Parse the input JSON data, to build the call data - callData, method, err := c.prepareDeployData(ctx, req) + callData, constructor, err := c.prepareDeployData(ctx, req) if err != nil { return nil, ffcapi.ErrorReasonInvalidInputs, err } @@ -45,10 +45,10 @@ func (c *ethConnector) DeployContractPrepare(ctx context.Context, req *ffcapi.Co return nil, ffcapi.ErrorReasonInvalidInputs, err } - if req.Gas, reason, err = c.ensureGasEstimate(ctx, tx, method, req.Gas); err != nil { + if req.Gas, reason, err = c.ensureGasEstimate(ctx, tx, constructor, req.Gas); err != nil { return nil, reason, err } - log.L(ctx).Infof("Prepared transaction method=%s dataLen=%d gas=%s", method.String(), len(callData), req.Gas.Int()) + log.L(ctx).Infof("Prepared deploy transaction dataLen=%d gas=%s", len(callData), req.Gas.Int()) return &ffcapi.TransactionPrepareResponse{ Gas: req.Gas, @@ -100,10 +100,9 @@ func (c *ethConnector) prepareDeployData(ctx context.Context, req *ffcapi.Contra // Note the FireFly ABI decoding package handles formatting errors / translation etc. var callData []byte paramValues, err := method.Inputs.ParseExternalDataCtx(ctx, ethParams) - if err != nil { - return nil, nil, err + if err == nil { + callData, err = paramValues.EncodeABIData() } - callData, err = paramValues.EncodeABIData() if err != nil { return nil, nil, err } diff --git a/internal/ethereum/deploy_contract_prepare_test.go b/internal/ethereum/deploy_contract_prepare_test.go index a788633..4fa593a 100644 --- a/internal/ethereum/deploy_contract_prepare_test.go +++ b/internal/ethereum/deploy_contract_prepare_test.go @@ -21,6 +21,7 @@ import ( "fmt" "testing" + "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-signer/pkg/ethtypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" @@ -91,4 +92,149 @@ func TestDeployContractPrepareWithEstimateRevert(t *testing.T) { assert.Equal(t, ffcapi.ErrorReasonTransactionReverted, reason) assert.Nil(t, res) + mRPC.AssertExpectations(t) + +} + +func TestDeployContractPrepareBadBytecode(t *testing.T) { + + ctx, c, _, done := newTestConnector(t) + defer done() + + var req ffcapi.ContractDeployPrepareRequest + err := json.Unmarshal([]byte(samplePrepareDeployTX), &req) + req.Contract = fftypes.JSONAnyPtr(`! not a string containing bytecode`) + assert.NoError(t, err) + _, reason, err := c.DeployContractPrepare(ctx, &req) + + assert.Regexp(t, "FF23047", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + +} + +func TestDeployContractPrepareBadBytecodeNotHex(t *testing.T) { + + ctx, c, _, done := newTestConnector(t) + defer done() + + var req ffcapi.ContractDeployPrepareRequest + err := json.Unmarshal([]byte(samplePrepareDeployTX), &req) + req.Contract = fftypes.JSONAnyPtr(`"!hex"`) + assert.NoError(t, err) + _, reason, err := c.DeployContractPrepare(ctx, &req) + + assert.Regexp(t, "FF23047", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + +} + +func TestDeployContractPrepareBadABIDefinition(t *testing.T) { + + ctx, c, _, done := newTestConnector(t) + defer done() + + var req ffcapi.ContractDeployPrepareRequest + err := json.Unmarshal([]byte(samplePrepareDeployTX), &req) + req.Definition = fftypes.JSONAnyPtr(`[`) + assert.NoError(t, err) + _, reason, err := c.DeployContractPrepare(ctx, &req) + + assert.Regexp(t, "FF23013", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + +} + +func TestDeployContractPrepareEstimateNoConstructor(t *testing.T) { + + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_estimateGas", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + *(args[1].(*ethtypes.HexInteger)) = *ethtypes.NewHexInteger64(12345) + }) + + var req ffcapi.ContractDeployPrepareRequest + err := json.Unmarshal([]byte(samplePrepareDeployTX), &req) + assert.NoError(t, err) + req.Definition = fftypes.JSONAnyPtr(`[]`) + req.Gas = nil + res, reason, err := c.DeployContractPrepare(ctx, &req) + + assert.NoError(t, err) + assert.Empty(t, reason) + + fGasEstimate, _ := c.gasEstimationFactor.Float64() + assert.Equal(t, int64(float64(12345)*fGasEstimate), res.Gas.Int64()) + + mRPC.AssertExpectations(t) + +} + +func TestDeployContractPrepareBadParamsJSON(t *testing.T) { + + ctx, c, _, done := newTestConnector(t) + defer done() + + var req ffcapi.ContractDeployPrepareRequest + err := json.Unmarshal([]byte(samplePrepareDeployTX), &req) + req.Params = []*fftypes.JSONAny{fftypes.JSONAnyPtr(`"!wrong`)} + assert.NoError(t, err) + _, reason, err := c.DeployContractPrepare(ctx, &req) + + assert.Regexp(t, "FF23014", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + +} + +func TestDeployContractPrepareBadParamType(t *testing.T) { + + ctx, c, _, done := newTestConnector(t) + defer done() + + var req ffcapi.ContractDeployPrepareRequest + err := json.Unmarshal([]byte(samplePrepareDeployTX), &req) + req.Params = []*fftypes.JSONAny{fftypes.JSONAnyPtr(`"!wrong"`)} + assert.NoError(t, err) + _, reason, err := c.DeployContractPrepare(ctx, &req) + + assert.Regexp(t, "FF22030", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + +} + +func TestDeployContractPrepareBadFrom(t *testing.T) { + + ctx, c, _, done := newTestConnector(t) + defer done() + + var req ffcapi.ContractDeployPrepareRequest + err := json.Unmarshal([]byte(samplePrepareDeployTX), &req) + req.From = "!not an address" + assert.NoError(t, err) + _, reason, err := c.DeployContractPrepare(ctx, &req) + + assert.Regexp(t, "FF23019", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + +} + +func TestDeployContractPrepareBadABIType(t *testing.T) { + + ctx, c, _, done := newTestConnector(t) + defer done() + + var req ffcapi.ContractDeployPrepareRequest + err := json.Unmarshal([]byte(samplePrepareDeployTX), &req) + req.Definition = fftypes.JSONAnyPtr(`[{ + "type": "constructor", + "inputs": [{ + "type": "!wrong" + }] + }]`) + assert.NoError(t, err) + _, reason, err := c.DeployContractPrepare(ctx, &req) + + assert.Regexp(t, "FF22025", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + }