From 915d260a0d9360c4eb8c50fd27743408188022a2 Mon Sep 17 00:00:00 2001 From: Dzianis Andreyenka Date: Mon, 6 Nov 2023 20:02:52 +0800 Subject: [PATCH 1/2] DA-536 Check transaction validity on the preparation step Signed-off-by: Dzianis Andreyenka --- internal/tezos/error_mapping.go | 5 + internal/tezos/exec_query.go | 15 ++ internal/tezos/prepare_transaction.go | 45 ++++- internal/tezos/prepare_transaction_test.go | 185 +++++++++++++++++++++ internal/tezos/send_transaction.go | 42 +---- internal/tezos/send_transaction_test.go | 79 --------- 6 files changed, 250 insertions(+), 121 deletions(-) diff --git a/internal/tezos/error_mapping.go b/internal/tezos/error_mapping.go index 20b1064..f4e73c2 100644 --- a/internal/tezos/error_mapping.go +++ b/internal/tezos/error_mapping.go @@ -11,6 +11,7 @@ type tezosRPCMethodCategory int const ( blockRPCMethods tezosRPCMethodCategory = iota + callRPCMethods sendRPCMethods ) @@ -26,6 +27,10 @@ func mapError(methodType tezosRPCMethodCategory, err error) ffcapi.ErrorReason { if strings.Contains(errString, "status 404") { return ffcapi.ErrorReasonNotFound } + case callRPCMethods: + if strings.Contains(errString, "script_rejected") { + return ffcapi.ErrorReasonTransactionReverted + } case sendRPCMethods: if strings.Contains(errString, "counter_in_the_past") { return ffcapi.ErrorReasonNonceTooLow diff --git a/internal/tezos/exec_query.go b/internal/tezos/exec_query.go index c83ebc1..c2a8803 100644 --- a/internal/tezos/exec_query.go +++ b/internal/tezos/exec_query.go @@ -3,6 +3,8 @@ package tezos import ( "context" + "blockwatch.cc/tzgo/codec" + "blockwatch.cc/tzgo/rpc" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) @@ -11,3 +13,16 @@ func (c *tezosConnector) QueryInvoke(_ context.Context, req *ffcapi.QueryInvokeR // TODO: to implement return nil, "", nil } + +func (c *tezosConnector) callTransaction(ctx context.Context, op *codec.Op, opts *rpc.CallOptions) (*rpc.Receipt, ffcapi.ErrorReason, error) { + sim, err := c.client.Simulate(ctx, op, opts) + if err != nil { + return nil, mapError(callRPCMethods, err), err + } + // fail with Tezos error when simulation failed + if !sim.IsSuccess() { + return nil, mapError(callRPCMethods, sim.Error()), sim.Error() + } + + return sim, "", nil +} diff --git a/internal/tezos/prepare_transaction.go b/internal/tezos/prepare_transaction.go index 30a5001..131ef44 100644 --- a/internal/tezos/prepare_transaction.go +++ b/internal/tezos/prepare_transaction.go @@ -16,12 +16,13 @@ import ( "blockwatch.cc/tzgo/tezos" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-tezosconnect/internal/msgs" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) // TransactionPrepare validates transaction inputs against the supplied schema/Michelson and performs any binary serialization required (prior to signing) to encode a transaction from JSON into the native blockchain format -func (c *tezosConnector) TransactionPrepare(ctx context.Context, req *ffcapi.TransactionPrepareRequest) (*ffcapi.TransactionPrepareResponse, ffcapi.ErrorReason, error) { +func (c *tezosConnector) TransactionPrepare(ctx context.Context, req *ffcapi.TransactionPrepareRequest) (res *ffcapi.TransactionPrepareResponse, reason ffcapi.ErrorReason, err error) { params, err := c.prepareInputParams(ctx, &req.TransactionInput) if err != nil { return nil, ffcapi.ErrorReasonInvalidInputs, err @@ -32,12 +33,54 @@ func (c *tezosConnector) TransactionPrepare(ctx context.Context, req *ffcapi.Tra return nil, "", err } + opts := &rpc.DefaultOptions + if reason, err = c.estimateAndAssignTxCost(ctx, op, opts); err != nil { + return nil, reason, err + } + log.L(ctx).Infof("Prepared transaction method=%s dataLen=%d", req.Method.String(), len(op.Bytes())) + return &ffcapi.TransactionPrepareResponse{ Gas: req.Gas, TransactionData: hex.EncodeToString(op.Bytes()), }, "", nil } +func (c *tezosConnector) estimateAndAssignTxCost(ctx context.Context, op *codec.Op, opts *rpc.CallOptions) (ffcapi.ErrorReason, error) { + // Simulate the transaction (dry run) + sim, reason, err := c.callTransaction(ctx, op, nil) + if err != nil { + return reason, err + } + + // apply simulated cost as limits to tx list + if !opts.IgnoreLimits { + op.WithLimits(sim.MinLimits(), rpc.ExtraSafetyMargin) + } + + // log info about tx costs + costs := sim.Costs() + for i, v := range op.Contents { + verb := "used" + if opts.IgnoreLimits { + verb = "forced" + } + limits := v.Limits() + log.L(ctx).Debugf("OP#%03d: %s gas_used(sim)=%d storage_used(sim)=%d storage_burn(sim)=%d alloc_burn(sim)=%d fee(%s)=%d gas_limit(%s)=%d storage_limit(%s)=%d ", + i, v.Kind(), costs[i].GasUsed, costs[i].StorageUsed, costs[i].StorageBurn, costs[i].AllocationBurn, + verb, limits.Fee, verb, limits.GasLimit, verb, limits.StorageLimit, + ) + } + + // check minFee calc against maxFee if set + if opts.MaxFee > 0 { + if l := op.Limits(); l.Fee > opts.MaxFee { + return "", fmt.Errorf("estimated cost %d > max %d", l.Fee, opts.MaxFee) + } + } + + return "", nil +} + func (c *tezosConnector) prepareInputParams(ctx context.Context, req *ffcapi.TransactionInput) (micheline.Parameters, error) { var tezosParams micheline.Parameters diff --git a/internal/tezos/prepare_transaction_test.go b/internal/tezos/prepare_transaction_test.go index 314ff5b..8cda76f 100644 --- a/internal/tezos/prepare_transaction_test.go +++ b/internal/tezos/prepare_transaction_test.go @@ -1,9 +1,12 @@ package tezos import ( + "encoding/json" "errors" "testing" + "blockwatch.cc/tzgo/codec" + "blockwatch.cc/tzgo/contract" "blockwatch.cc/tzgo/rpc" "blockwatch.cc/tzgo/tezos" "github.com/hyperledger/firefly-common/pkg/fftypes" @@ -25,6 +28,25 @@ func TestTransactionPrepareOk(t *testing.T) { Manager: "edpkv89Jj4aVWetK69CWm5ss1LayvK8dQoiFz7p995y1k3E8CZwqJ6", }, nil) + mRPC.On("Simulate", ctx, mock.Anything, mock.Anything). + Return(&rpc.Receipt{ + Op: &rpc.Operation{ + Contents: []rpc.TypedOperation{ + rpc.Transaction{ + Manager: rpc.Manager{ + Generic: rpc.Generic{ + Metadata: rpc.OperationMetadata{ + Result: rpc.OperationResult{ + Status: tezos.OpStatusApplied, + }, + }, + }, + }, + }, + }, + }, + }, nil) + req := &ffcapi.TransactionPrepareRequest{ TransactionInput: ffcapi.TransactionInput{ TransactionHeaders: ffcapi.TransactionHeaders{ @@ -133,3 +155,166 @@ func TestTransactionPrepareGetContractExtError(t *testing.T) { _, _, err := c.TransactionPrepare(ctx, req) assert.Error(t, err) } + +func TestTransactionPrepareSimulateError(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("GetBlockHash", ctx, mock.Anything). + Return(tezos.NewBlockHash([]byte("BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg")), nil) + + mRPC.On("GetContractExt", ctx, mock.Anything, mock.Anything). + Return(&rpc.ContractInfo{ + Counter: 10, + Manager: "edpkv89Jj4aVWetK69CWm5ss1LayvK8dQoiFz7p995y1k3E8CZwqJ6", + }, nil) + + mRPC.On("Simulate", ctx, mock.Anything, mock.Anything).Return(nil, errors.New("error")) + + req := &ffcapi.TransactionPrepareRequest{ + TransactionInput: ffcapi.TransactionInput{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN", + To: "KT1D254HTPKq5GZNVcF73XBinG9BLybHqu8s", + }, + Method: fftypes.JSONAnyPtr("\"pause\""), + Params: []*fftypes.JSONAny{ + fftypes.JSONAnyPtr("{\"entrypoint\":\"pause\",\"value\":{\"prim\":\"True\"}}"), + }, + }, + } + + _, _, err := c.TransactionPrepare(ctx, req) + assert.Error(t, err) +} + +func TestTransactionPrepareWrongSimulateStatusError(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("GetBlockHash", ctx, mock.Anything). + Return(tezos.NewBlockHash([]byte("BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg")), nil) + + mRPC.On("GetContractExt", ctx, mock.Anything, mock.Anything). + Return(&rpc.ContractInfo{ + Counter: 10, + Manager: "edpkv89Jj4aVWetK69CWm5ss1LayvK8dQoiFz7p995y1k3E8CZwqJ6", + }, nil) + + mRPC.On("Simulate", ctx, mock.Anything, mock.Anything). + Return(&rpc.Receipt{ + Op: &rpc.Operation{ + Contents: []rpc.TypedOperation{ + rpc.Transaction{ + Manager: rpc.Manager{ + Generic: rpc.Generic{ + Metadata: rpc.OperationMetadata{ + Result: rpc.OperationResult{ + Errors: []rpc.OperationError{ + { + GenericError: rpc.GenericError{ + ID: "error id: script_rejected", + Kind: "error: script_rejected", + }, + Raw: json.RawMessage{}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, nil) + + req := &ffcapi.TransactionPrepareRequest{ + TransactionInput: ffcapi.TransactionInput{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN", + To: "KT1D254HTPKq5GZNVcF73XBinG9BLybHqu8s", + }, + Method: fftypes.JSONAnyPtr("\"pause\""), + Params: []*fftypes.JSONAny{ + fftypes.JSONAnyPtr("{\"entrypoint\":\"pause\",\"value\":{\"prim\":\"True\"}}"), + }, + }, + } + + _, reason, err := c.TransactionPrepare(ctx, req) + assert.Error(t, err) + assert.Equal(t, reason, ffcapi.ErrorReasonTransactionReverted) +} + +func Test_estimateAndAssignTxCostIgnoreLimitsOk(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("Simulate", ctx, mock.Anything, mock.Anything). + Return(&rpc.Receipt{ + Op: &rpc.Operation{ + Contents: []rpc.TypedOperation{ + rpc.Transaction{ + Manager: rpc.Manager{ + Generic: rpc.Generic{ + Metadata: rpc.OperationMetadata{ + Result: rpc.OperationResult{ + Status: tezos.OpStatusApplied, + }, + }, + }, + }, + }, + }, + }, + }, nil) + + op := codec.NewOp() + txArgs := contract.TxArgs{} + op.WithContents(txArgs.Encode()) + + opts := &rpc.DefaultOptions + opts.IgnoreLimits = true + + _, err := c.estimateAndAssignTxCost(ctx, op, opts) + assert.NoError(t, err) +} + +func Test_estimateAndAssignExceedMaxLimitError(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mRPC.On("Simulate", ctx, mock.Anything, mock.Anything). + Return(&rpc.Receipt{ + Op: &rpc.Operation{ + Contents: []rpc.TypedOperation{ + rpc.Transaction{ + Manager: rpc.Manager{ + Generic: rpc.Generic{ + Metadata: rpc.OperationMetadata{ + Result: rpc.OperationResult{ + Status: tezos.OpStatusApplied, + }, + }, + }, + }, + }, + }, + }, + }, nil) + + op := codec.NewOp() + txArgs := contract.TxArgs{} + op.WithContents(txArgs.Encode()) + op.WithLimits([]tezos.Limits{ + { + Fee: 100, + }, + }, 100) + + opts := &rpc.DefaultOptions + opts.MaxFee = 1 + + _, err := c.estimateAndAssignTxCost(ctx, op, opts) + assert.Error(t, err) +} diff --git a/internal/tezos/send_transaction.go b/internal/tezos/send_transaction.go index f0413b3..e8dac73 100644 --- a/internal/tezos/send_transaction.go +++ b/internal/tezos/send_transaction.go @@ -10,9 +10,7 @@ import ( "net/http" "blockwatch.cc/tzgo/codec" - "blockwatch.cc/tzgo/rpc" "blockwatch.cc/tzgo/tezos" - "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) @@ -34,44 +32,6 @@ func (c *tezosConnector) TransactionSend(ctx context.Context, req *ffcapi.Transa return nil, "", err } - opts := &rpc.DefaultOptions - - // simulate to check tx validity and estimate cost - sim, err := c.client.Simulate(ctx, op, opts) - if err != nil { - return nil, mapError(sendRPCMethods, err), err - } - // fail with Tezos error when simulation failed - if !sim.IsSuccess() { - return nil, "", sim.Error() - } - - // apply simulated cost as limits to tx list - if !opts.IgnoreLimits { - op.WithLimits(sim.MinLimits(), rpc.ExtraSafetyMargin) - } - - // log info about tx costs - costs := sim.Costs() - for i, v := range op.Contents { - verb := "used" - if opts.IgnoreLimits { - verb = "forced" - } - limits := v.Limits() - log.L(ctx).Debugf("OP#%03d: %s gas_used(sim)=%d storage_used(sim)=%d storage_burn(sim)=%d alloc_burn(sim)=%d fee(%s)=%d gas_limit(%s)=%d storage_limit(%s)=%d ", - i, v.Kind(), costs[i].GasUsed, costs[i].StorageUsed, costs[i].StorageBurn, costs[i].AllocationBurn, - verb, limits.Fee, verb, limits.GasLimit, verb, limits.StorageLimit, - ) - } - - // check minFee calc against maxFee if set - if opts.MaxFee > 0 { - if l := op.Limits(); l.Fee > opts.MaxFee { - return nil, "", fmt.Errorf("estimated cost %d > max %d", l.Fee, opts.MaxFee) - } - } - // sign tx err = c.signTxRemotely(ctx, op) if err != nil { @@ -89,7 +49,7 @@ func (c *tezosConnector) TransactionSend(ctx context.Context, req *ffcapi.Transa }, "", nil } -func (c *tezosConnector) signTxRemotely(ctx context.Context, op *codec.Op) error { +func (c *tezosConnector) signTxRemotely(_ context.Context, op *codec.Op) error { url := c.signatoryURL + "/keys/" + op.Source.String() requestBody, _ := json.Marshal(hex.EncodeToString(op.WatermarkedBytes())) diff --git a/internal/tezos/send_transaction_test.go b/internal/tezos/send_transaction_test.go index 26031af..dbeed43 100644 --- a/internal/tezos/send_transaction_test.go +++ b/internal/tezos/send_transaction_test.go @@ -1,11 +1,9 @@ package tezos import ( - "encoding/json" "errors" "testing" - "blockwatch.cc/tzgo/rpc" "blockwatch.cc/tzgo/tezos" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" @@ -72,80 +70,3 @@ func TestTransactionSendGetContractExtError(t *testing.T) { _, _, err := c.TransactionSend(ctx, req) assert.Error(t, err) } - -func TestTransactionSendSimulateError(t *testing.T) { - ctx, c, mRPC, done := newTestConnector(t) - defer done() - - mRPC.On("GetBlockHash", ctx, mock.Anything). - Return(tezos.NewBlockHash([]byte("BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg")), nil) - - mRPC.On("GetContractExt", ctx, mock.Anything, mock.Anything). - Return(&rpc.ContractInfo{ - Counter: 10, - Manager: "edpkv89Jj4aVWetK69CWm5ss1LayvK8dQoiFz7p995y1k3E8CZwqJ6", - }, nil) - - mRPC.On("Simulate", ctx, mock.Anything, mock.Anything).Return(nil, errors.New("error")) - - req := &ffcapi.TransactionSendRequest{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN", - To: "KT1D254HTPKq5GZNVcF73XBinG9BLybHqu8s", - }, - TransactionData: "424d426559724d4a704c577271437337555463466155514365574271736a434c6c00889816a17ae688c971be1ad34bfe1990f8fa5e0f000b0000000130a980e6e41028da2cacfca4ddefea252d18bed900ffff05706175736500000002030a", - } - _, _, err := c.TransactionSend(ctx, req) - assert.Error(t, err) -} - -func TestTransactionSendWrongSimulateStatusError(t *testing.T) { - ctx, c, mRPC, done := newTestConnector(t) - defer done() - - mRPC.On("GetBlockHash", ctx, mock.Anything). - Return(tezos.NewBlockHash([]byte("BMBeYrMJpLWrqCs7UTcFaUQCeWBqsjCLejX5D8zE8m9syHqHnZg")), nil) - - mRPC.On("GetContractExt", ctx, mock.Anything, mock.Anything). - Return(&rpc.ContractInfo{ - Counter: 10, - Manager: "edpkv89Jj4aVWetK69CWm5ss1LayvK8dQoiFz7p995y1k3E8CZwqJ6", - }, nil) - - mRPC.On("Simulate", ctx, mock.Anything, mock.Anything). - Return(&rpc.Receipt{ - Op: &rpc.Operation{ - Contents: []rpc.TypedOperation{ - rpc.Transaction{ - Manager: rpc.Manager{ - Generic: rpc.Generic{ - Metadata: rpc.OperationMetadata{ - Result: rpc.OperationResult{ - Errors: []rpc.OperationError{ - { - GenericError: rpc.GenericError{ - ID: "error id", - Kind: "error", - }, - Raw: json.RawMessage{}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, nil) - - req := &ffcapi.TransactionSendRequest{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "tz1Y6GnVhC4EpcDDSmD3ibcC4WX6DJ4Q1QLN", - To: "KT1D254HTPKq5GZNVcF73XBinG9BLybHqu8s", - }, - TransactionData: "424d426559724d4a704c577271437337555463466155514365574271736a434c6c00889816a17ae688c971be1ad34bfe1990f8fa5e0f000b0000000130a980e6e41028da2cacfca4ddefea252d18bed900ffff05706175736500000002030a", - } - _, _, err := c.TransactionSend(ctx, req) - assert.Error(t, err) -} From b3a589d71376108f80a62d1aefbd6fb1e154f4bf Mon Sep 17 00:00:00 2001 From: Dzianis Andreyenka Date: Mon, 6 Nov 2023 13:21:27 +0100 Subject: [PATCH 2/2] Update send_transaction.go Remove empty line Signed-off-by: Dzianis Andreyenka --- internal/tezos/send_transaction.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/tezos/send_transaction.go b/internal/tezos/send_transaction.go index 57369d1..e8dac73 100644 --- a/internal/tezos/send_transaction.go +++ b/internal/tezos/send_transaction.go @@ -49,7 +49,6 @@ func (c *tezosConnector) TransactionSend(ctx context.Context, req *ffcapi.Transa }, "", nil } - func (c *tezosConnector) signTxRemotely(_ context.Context, op *codec.Op) error { url := c.signatoryURL + "/keys/" + op.Source.String() requestBody, _ := json.Marshal(hex.EncodeToString(op.WatermarkedBytes()))