Repository: nspcc-dev/neo-go
Pull request #3360 (2024-03-18 15:25:13)

rpcsrv: fix RPC error codes proposal compatibility
### Problem

Implementation doesn't match https://github.com/neo-project/proposals/pull/156 wrt mempool-related transaction fee check on submitting new transaction to the node. Thanks to @carpawell for pointing that out.

### Solution

Fix the implementation.

@roman-khimov, I'd like to highlight: whereas it's quite easy to fix this compatibility problem on our side, C# node has the same divergence from the standard and requires more changes to fix it: `TransactionVerificationContext.CheckTransaction` should be adjusted to return some new special error, this error should be detached from `VerifyResult.InsufficientFunds` and ported to other components (ref. https://github.com/neo-project/neo/blob/b05501af882a0d1f2a1a7841c6ddc4d0504e5fc1/src/Neo/Network/P2P/Payloads/Transaction.cs#L368).

I may port it to C# or create an issue about it, what would we prefer?


Patch:
diff --git a/pkg/services/rpcsrv/server.go b/pkg/services/rpcsrv/server.go
index 6430a2ffe..545418234 100644
--- a/pkg/services/rpcsrv/server.go
+++ b/pkg/services/rpcsrv/server.go
@@ -2657,7 +2657,7 @@ func getRelayResult(err error, hash util.Uint256) (any, *neorpc.Error) {
 		return nil, neorpc.WrapErrorWithData(neorpc.ErrInsufficientNetworkFee, err.Error())
 	case errors.Is(err, core.ErrInvalidAttribute):
 		return nil, neorpc.WrapErrorWithData(neorpc.ErrInvalidAttribute, err.Error())
-	case errors.Is(err, core.ErrInsufficientFunds):
+	case errors.Is(err, core.ErrMemPoolConflict):
 		return nil, neorpc.WrapErrorWithData(neorpc.ErrInsufficientFunds, err.Error())
 	case errors.Is(err, core.ErrInvalidSignature):
 		return nil, neorpc.WrapErrorWithData(neorpc.ErrInvalidSignature, err.Error())


Test patch:
diff --git a/pkg/services/rpcsrv/server_test.go b/pkg/services/rpcsrv/server_test.go
index c0b0fe151..1b275d92c 100644
--- a/pkg/services/rpcsrv/server_test.go
+++ b/pkg/services/rpcsrv/server_test.go
@@ -2659,7 +2659,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
 		t.Run("insufficient funds", func(t *testing.T) {
 			b := testchain.NewBlock(t, chain, 1, 0, newTxWithParams(t, chain, opcode.PUSH1, 10, 899999999999, 1, false))
 			body := doRPCCall(fmt.Sprintf(rpc, encodeBinaryToString(t, b)), httpSrv.URL, t)
-			checkErrGetResult(t, body, true, neorpc.ErrInsufficientFundsCode)
+			checkErrGetResult(t, body, true, neorpc.ErrVerificationFailedCode)
 		})
 		t.Run("positive", func(t *testing.T) {
 			b := testchain.NewBlock(t, chain, 1, 0, newTxWithParams(t, chain, opcode.PUSH1, 10, 0, 1, false))
@@ -3341,6 +3341,23 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
 			body2 := doRPCCall(fmt.Sprintf(rpc, rawTx2), httpSrv.URL, t)
 			checkErrGetResult(t, body2, true, neorpc.ErrMempoolCapReachedCode)
 		})
+		t.Run("mempool conflict", func(t *testing.T) {
+			chain, _, httpSrv := initClearServerWithCustomConfig(t, func(c *config.Config) {
+				c.ProtocolConfiguration.MemPoolSize = 2
+			})
+
+			// Create and push the first transaction with large network fee.
+			tx := newTxWithParams(t, chain, opcode.PUSH1, 10, 1, 25, false)
+			rawTx := encodeBinaryToString(t, tx)
+			body := doRPCCall(fmt.Sprintf(rpc, rawTx), httpSrv.URL, t)
+			checkErrGetResult(t, body, false, 0)
+
+			// Create and push the second transaction, sender doesn't have enough balance to pay for two transactions.
+			tx2 := newTxWithParams(t, chain, opcode.PUSH1, 10, 1, 25, false)
+			rawTx2 := encodeBinaryToString(t, tx2)
+			body2 := doRPCCall(fmt.Sprintf(rpc, rawTx2), httpSrv.URL, t)
+			checkErrGetResult(t, body2, true, neorpc.ErrInsufficientFundsCode)
+		})
 	})
 	t.Run("test functions with unsupported states", func(t *testing.T) {
 		chain, _, httpSrv := initClearServerWithCustomConfig(t, func(c *config.Config) {
@@ -3380,7 +3397,7 @@ func newTxWithParams(t *testing.T, chain *core.Blockchain, code opcode.Opcode, v
 
 	height := chain.BlockHeight()
 	tx := transaction.New([]byte{byte(code)}, 0)
-	tx.Nonce = height + 1
+	tx.Nonce = uint32(random.Int(0, math.MaxUint32))
 	tx.ValidUntilBlock = height + validUntilIncr
 	tx.Signers = []transaction.Signer{{Account: acc0.PrivateKey().GetScriptHash()}}
 	tx.SystemFee = systemFee