From 2b11e99225850bd9f51e50a1e26cf11dfc89b632 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 30 Sep 2020 13:20:40 +0300 Subject: [PATCH 1/2] core: fix CalculateNetworkFee for signature contracts First PUSHDATA1 is from invocation script, the second PUSHDATA1 is from verification script. E.g.: Invocation script: INDEX OPCODE PARAMETER 0 PUSHDATA1 035913b9588da23a5c3ce14b2886a6b8ebb6a0eb92bdaa948510dfb5ae5194d6cb << 35 PUSHNULL 36 SYSCALL Neo.Crypto.VerifyWithECDsaSecp256r1 (95440d78) Verification script: INDEX OPCODE PARAMETER 0 PUSHDATA1 3930fe5a9b44682f37741955df4a5f2585ed5aa438fa6e17ae51083673b1d64253e5a859c0cf168be67971e53a23c1c40582777d94a8e391db23ff613849627d << --- pkg/core/blockchain_test.go | 49 ++++++++++++++++++++++++++++++++----- pkg/core/util.go | 2 +- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 2347f03cc..78de1c9f9 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -215,7 +215,7 @@ func TestVerifyTx(t *testing.T) { bc := newTestChain(t) defer bc.Close() - accs := make([]*wallet.Account, 4) + accs := make([]*wallet.Account, 5) for i := range accs { var err error accs[i], err = wallet.NewAccount() @@ -291,11 +291,48 @@ func TestVerifyTx(t *testing.T) { require.NoError(t, accs[0].SignTx(tx)) checkErr(t, ErrTxTooBig, tx) }) - t.Run("SmallNetworkFee", func(t *testing.T) { - tx := bc.newTestTx(h, testScript) - tx.NetworkFee = 1 - require.NoError(t, accs[0].SignTx(tx)) - checkErr(t, ErrTxSmallNetworkFee, tx) + t.Run("NetworkFee", func(t *testing.T) { + t.Run("SmallNetworkFee", func(t *testing.T) { + tx := bc.newTestTx(h, testScript) + tx.NetworkFee = 1 + require.NoError(t, accs[0].SignTx(tx)) + checkErr(t, ErrTxSmallNetworkFee, tx) + }) + t.Run("CalculateNetworkFee, signature script", func(t *testing.T) { + tx := bc.newTestTx(h, testScript) + expectedSize := io.GetVarSize(tx) + verificationNetFee, calculatedScriptSize := CalculateNetworkFee(accs[0].Contract.Script) + expectedSize += calculatedScriptSize + expectedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() + tx.NetworkFee = expectedNetFee + require.NoError(t, accs[0].SignTx(tx)) + actualSize := io.GetVarSize(tx) + require.Equal(t, expectedSize, actualSize) + interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, nil, tx) + gasConsumed, err := bc.verifyHashAgainstScript(h, &tx.Scripts[0], interopCtx, -1) + require.NoError(t, err) + require.Equal(t, verificationNetFee, gasConsumed) + require.Equal(t, expectedNetFee, bc.FeePerByte()*int64(actualSize)+gasConsumed) + }) + t.Run("CalculateNetworkFee, multisignature script", func(t *testing.T) { + multisigAcc := accs[4] + pKeys := keys.PublicKeys{multisigAcc.PrivateKey().PublicKey()} + require.NoError(t, multisigAcc.ConvertMultisig(1, pKeys)) + multisigHash := hash.Hash160(multisigAcc.Contract.Script) + tx := bc.newTestTx(multisigHash, testScript) + verificationNetFee, calculatedScriptSize := CalculateNetworkFee(multisigAcc.Contract.Script) + expectedSize := io.GetVarSize(tx) + calculatedScriptSize + expectedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() + tx.NetworkFee = expectedNetFee + require.NoError(t, multisigAcc.SignTx(tx)) + actualSize := io.GetVarSize(tx) + require.Equal(t, expectedSize, actualSize) + interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, nil, tx) + gasConsumed, err := bc.verifyHashAgainstScript(multisigHash, &tx.Scripts[0], interopCtx, -1) + require.NoError(t, err) + require.Equal(t, verificationNetFee, gasConsumed) + require.Equal(t, expectedNetFee, bc.FeePerByte()*int64(actualSize)+gasConsumed) + }) }) t.Run("Conflict", func(t *testing.T) { balance := bc.GetUtilityTokenBalance(h).Int64() diff --git a/pkg/core/util.go b/pkg/core/util.go index 9ce81e47b..973c74c46 100644 --- a/pkg/core/util.go +++ b/pkg/core/util.go @@ -138,7 +138,7 @@ func CalculateNetworkFee(script []byte) (int64, int) { ) if vm.IsSignatureContract(script) { size += 67 + io.GetVarSize(script) - netFee += opcodePrice(opcode.PUSHDATA1, opcode.PUSHNULL) + crypto.ECDSAVerifyPrice + netFee += opcodePrice(opcode.PUSHDATA1, opcode.PUSHNULL, opcode.PUSHDATA1) + crypto.ECDSAVerifyPrice } else if m, pubs, ok := vm.ParseMultiSigContract(script); ok { n := len(pubs) sizeInv := 66 * m From e34e367a7b2d999988d8d5119c785972197fbfd4 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 30 Sep 2020 13:50:58 +0300 Subject: [PATCH 2/2] core: take into account size fee during verifyTxWitnesses GasLimit for transaction scripts verification should not include fee for transaction size. --- pkg/core/blockchain.go | 2 +- pkg/core/blockchain_test.go | 20 ++++++++++++++++++++ pkg/core/helper_test.go | 2 +- pkg/rpc/server/server_test.go | 14 +++++++------- pkg/rpc/server/testdata/testblocks.acc | Bin 7725 -> 7577 bytes 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index e8df34de1..5aa3277c9 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1554,7 +1554,7 @@ func (bc *Blockchain) verifyTxWitnesses(t *transaction.Transaction, block *block return fmt.Errorf("%w: %d vs %d", ErrTxInvalidWitnessNum, len(t.Signers), len(t.Scripts)) } interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, block, t) - gasLimit := t.NetworkFee + gasLimit := t.NetworkFee - int64(t.Size())*bc.FeePerByte() for i := range t.Signers { gasConsumed, err := bc.verifyHashAgainstScript(t.Signers[i].Account, &t.Scripts[i], interopCtx, gasLimit) if err != nil { diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 78de1c9f9..98bd8b04a 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -298,6 +298,26 @@ func TestVerifyTx(t *testing.T) { require.NoError(t, accs[0].SignTx(tx)) checkErr(t, ErrTxSmallNetworkFee, tx) }) + t.Run("AlmostEnoughNetworkFee", func(t *testing.T) { + tx := bc.newTestTx(h, testScript) + verificationNetFee, calcultedScriptSize := CalculateNetworkFee(accs[0].Contract.Script) + expectedSize := io.GetVarSize(tx) + calcultedScriptSize + calculatedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() + tx.NetworkFee = calculatedNetFee - 1 + require.NoError(t, accs[0].SignTx(tx)) + require.Equal(t, expectedSize, io.GetVarSize(tx)) + checkErr(t, ErrVerificationFailed, tx) + }) + t.Run("EnoughNetworkFee", func(t *testing.T) { + tx := bc.newTestTx(h, testScript) + verificationNetFee, calcultedScriptSize := CalculateNetworkFee(accs[0].Contract.Script) + expectedSize := io.GetVarSize(tx) + calcultedScriptSize + calculatedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte() + tx.NetworkFee = calculatedNetFee + require.NoError(t, accs[0].SignTx(tx)) + require.Equal(t, expectedSize, io.GetVarSize(tx)) + require.NoError(t, bc.VerifyTx(tx)) + }) t.Run("CalculateNetworkFee, signature script", func(t *testing.T) { tx := bc.newTestTx(h, testScript) expectedSize := io.GetVarSize(tx) diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 1293365e9..e918bde1f 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -206,7 +206,7 @@ func TestCreateBasicChain(t *testing.T) { bw := io.NewBufBinWriter() b.EncodeBinary(bw.BinWriter) require.NoError(t, bw.Err) - t.Logf("Block1 hex: %s", bw.Bytes()) + t.Logf("Block1 hex: %s", hex.EncodeToString(bw.Bytes())) t.Logf("txMoveNeo hash: %s", txMoveNeo.Hash().StringLE()) t.Logf("txMoveNeo hex: %s", hex.EncodeToString(txMoveNeo.Bytes())) t.Logf("txMoveGas hash: %s", txMoveGas.Hash().StringLE()) diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 8b66cc3c5..1f1b121a7 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -54,11 +54,11 @@ type rpcTestCase struct { check func(t *testing.T, e *executor, result interface{}) } -const testContractHash = "4546ec6fcdaa1c3ccdb048526b78624b457b60a4" -const deploymentTxHash = "17be1bbb0fdecae18cd4c6a2db19311f47bd540371e2ea479a46b349a66aa0b3" +const testContractHash = "b0fda4dd46b8e5d207e86e774a4a133c6db69ee7" +const deploymentTxHash = "59f7b22b90e26f883a56b916c1580e3ee4f13caded686353cd77577e6194c173" -const verifyContractHash = "47ef649f9a77cad161ddaa28b39c7e450e5429e7" -const verifyContractAVM = "560340570300412d510830db4121700c14aa8acf859d4fe402b34e673f2156821796a488ebdb30716813cedb2869db289740" +const verifyContractHash = "c1213693b22cb0454a436d6e0bd76b8c0a3bfdf7" +const verifyContractAVM = "570300412d51083021700c14aa8acf859d4fe402b34e673f2156821796a488ebdb30716813cedb2869db289740" var rpcTestCases = map[string][]rpcTestCase{ "getapplicationlog": { @@ -643,12 +643,12 @@ var rpcTestCases = map[string][]rpcTestCase{ "sendrawtransaction": { { name: "positive", - params: `["000a0000008096980000000000721b130000000000b004000001aa8acf859d4fe402b34e673f2156821796a488eb01005d0300e87648170000000c1478ba4c24009fe510e136c9995a2e05215e1be4dc0c14aa8acf859d4fe402b34e673f2156821796a488eb13c00c087472616e736665720c1425059ecb4878d3a875f91c51ceded330d4575fde41627d5b523801420c40b99503c74bb1861b0b45060501dd090224f6c404aca8c02ccba3243c9b9691c1ef9e6b824d731f8fab27c56ba75609d32d2d176e97f56d9e3780610c83ebd41a290c2102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc20b4195440d78"]`, + params: `["000b0000008096980000000000261c130000000000b004000001aa8acf859d4fe402b34e673f2156821796a488eb01005d0300e87648170000000c1478ba4c24009fe510e136c9995a2e05215e1be4dc0c14aa8acf859d4fe402b34e673f2156821796a488eb13c00c087472616e736665720c1425059ecb4878d3a875f91c51ceded330d4575fde41627d5b523801420c40ea2f56acf7f64629dc922d65a60176f3963afd4b7c259f2017a3a5139346f8ea54704624590832acb7794069ab2983ddc862b03b6a33d4428cd4c45cbc0941c2290c2102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc20b4195440d78"]`, result: func(e *executor) interface{} { return &result.RelayResult{} }, check: func(t *testing.T, e *executor, inv interface{}) { res, ok := inv.(*result.RelayResult) require.True(t, ok) - expectedHash, err := util.Uint256DecodeStringLE("8b6e610a2205914411b26c4380594fa9a1e16961ff5896ed3b16831a151c6dd0") + expectedHash, err := util.Uint256DecodeStringLE("ab5573cfc8d70774f04aa7d5521350cfc1aa1239c44c24e490e139408cd46a57") require.NoError(t, err) assert.Equal(t, expectedHash, res.Hash) }, @@ -1075,7 +1075,7 @@ func checkNep5Balances(t *testing.T, e *executor, acc interface{}) { }, { Asset: e.chain.UtilityTokenHash(), - Amount: "799.59495030", + Amount: "799.59641770", LastUpdated: 7, }}, Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(), diff --git a/pkg/rpc/server/testdata/testblocks.acc b/pkg/rpc/server/testdata/testblocks.acc index decc2c137cf5a26a76a83817be07e93e54d91c3f..722d3c9a88c6b68ab978a98ad131d035eaeeea2a 100644 GIT binary patch delta 2998 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 delta 3046 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