rpcsrv: use stricter GAS limit for calculatenetworkfee

Valid transactions can't use more than MaxVerificationGAS for script execution
and this applies to the whole set of signers, so use this value by default
unless local instance configuration suggests something lower for generic
invocations.

Signed-off-by: Roman Khimov <roman@nspcc.ru>
This commit is contained in:
Roman Khimov 2023-09-27 19:31:21 +03:00
parent fd2774ea91
commit 49a44b0b9d
3 changed files with 45 additions and 3 deletions

View file

@ -233,7 +233,9 @@ where:
proxy can be used to have proper app-specific CORS settings), but it's an proxy can be used to have proper app-specific CORS settings), but it's an
easy way to make RPC interface accessible from the browser. easy way to make RPC interface accessible from the browser.
- `MaxGasInvoke` is the maximum GAS allowed to spend during `invokefunction` and - `MaxGasInvoke` is the maximum GAS allowed to spend during `invokefunction` and
`invokescript` RPC-calls. `invokescript` RPC-calls. `calculatenetworkfee` also can't exceed this GAS amount
(normally the limit for it is MaxVerificationGAS from Policy, but if MaxGasInvoke
is lower than that then this limit is respected).
- `MaxIteratorResultItems` - maximum number of elements extracted from iterator - `MaxIteratorResultItems` - maximum number of elements extracted from iterator
returned by `invoke*` call. When the `MaxIteratorResultItems` value is set to returned by `invoke*` call. When the `MaxIteratorResultItems` value is set to
`n`, only `n` iterations are returned and truncated is true, indicating that `n`, only `n` iterations are returned and truncated is true, indicating that

View file

@ -917,7 +917,15 @@ func (s *Server) calculateNetworkFee(reqParams params.Params) (any, *neorpc.Erro
return 0, neorpc.WrapErrorWithData(neorpc.ErrInvalidParams, fmt.Sprintf("failed to compute tx size: %s", err)) return 0, neorpc.WrapErrorWithData(neorpc.ErrInvalidParams, fmt.Sprintf("failed to compute tx size: %s", err))
} }
size := len(hashablePart) + io.GetVarSize(len(tx.Signers)) size := len(hashablePart) + io.GetVarSize(len(tx.Signers))
var netFee int64 var (
netFee int64
// Verification GAS cost can't exceed this policy.
gasLimit = s.chain.GetMaxVerificationGAS()
)
if gasLimit > int64(s.config.MaxGasInvoke) {
// But we honor instance configuration as well.
gasLimit = int64(s.config.MaxGasInvoke)
}
for i, signer := range tx.Signers { for i, signer := range tx.Signers {
w := tx.Scripts[i] w := tx.Scripts[i]
if len(w.InvocationScript) == 0 { // No invocation provided, try to infer one. if len(w.InvocationScript) == 0 { // No invocation provided, try to infer one.
@ -951,10 +959,11 @@ func (s *Server) calculateNetworkFee(reqParams params.Params) (any, *neorpc.Erro
} }
w.InvocationScript = inv.Bytes() w.InvocationScript = inv.Bytes()
} }
gasConsumed, err := s.chain.VerifyWitness(signer.Account, tx, &w, int64(s.config.MaxGasInvoke)) gasConsumed, err := s.chain.VerifyWitness(signer.Account, tx, &w, gasLimit)
if err != nil && !errors.Is(err, core.ErrInvalidSignature) { if err != nil && !errors.Is(err, core.ErrInvalidSignature) {
return nil, neorpc.WrapErrorWithData(neorpc.ErrInvalidSignature, err.Error()) return nil, neorpc.WrapErrorWithData(neorpc.ErrInvalidSignature, err.Error())
} }
gasLimit -= gasConsumed
netFee += gasConsumed netFee += gasConsumed
size += io.GetVarSize(w.VerificationScript) + io.GetVarSize(w.InvocationScript) size += io.GetVarSize(w.VerificationScript) + io.GetVarSize(w.InvocationScript)
} }

View file

@ -3,6 +3,7 @@ package rpcsrv
import ( import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"encoding/binary"
"encoding/json" "encoding/json"
"fmt" "fmt"
gio "io" gio "io"
@ -26,6 +27,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core"
"github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/fee"
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/storage/dboper" "github.com/nspcc-dev/neo-go/pkg/core/storage/dboper"
@ -3184,6 +3186,21 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
body := calcReq(t, tx) body := calcReq(t, tx)
_ = checkErrGetResult(t, body, true, neorpc.ErrInvalidVerificationFunctionCode, "signer 0 has no verify method in deployed contract") _ = checkErrGetResult(t, body, true, neorpc.ErrInvalidVerificationFunctionCode, "signer 0 has no verify method in deployed contract")
}) })
t.Run("execution limit, fail", func(t *testing.T) {
// 1_6000_0000 GAS with the default 1.5 allowed by Policy
verifScript := []byte{byte(opcode.PUSHINT32), 0x00, 0x58, 0x89, 0x09, byte(opcode.SYSCALL), 0, 0, 0, 0, byte(opcode.PUSHT)}
binary.LittleEndian.PutUint32(verifScript[6:], interopnames.ToID([]byte(interopnames.SystemRuntimeBurnGas)))
tx := &transaction.Transaction{
Script: []byte{byte(opcode.RET)},
Signers: []transaction.Signer{{Account: hash.Hash160(verifScript)}},
Scripts: []transaction.Witness{{
InvocationScript: []byte{byte(opcode.NOP)},
VerificationScript: verifScript,
}},
}
body := calcReq(t, tx)
_ = checkErrGetResult(t, body, true, neorpc.ErrInvalidSignatureCode, "GAS limit exceeded")
})
checkCalc := func(t *testing.T, tx *transaction.Transaction, fee int64) { checkCalc := func(t *testing.T, tx *transaction.Transaction, fee int64) {
resp := checkErrGetResult(t, calcReq(t, tx), false, 0) resp := checkErrGetResult(t, calcReq(t, tx), false, 0)
res := new(result.NetworkFee) res := new(result.NetworkFee)
@ -3258,6 +3275,20 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
invocScript := invocWriter.Bytes() invocScript := invocWriter.Bytes()
checkContract(t, verAcc, invocScript, 146960) // No C# match, but we believe it's OK and it has a specific invocation script overriding anything server-side. checkContract(t, verAcc, invocScript, 146960) // No C# match, but we believe it's OK and it has a specific invocation script overriding anything server-side.
}) })
t.Run("execution limit, ok", func(t *testing.T) {
// 1_4000_0000 GAS with the default 1.5 allowed by Policy
verifScript := []byte{byte(opcode.PUSHINT32), 0x00, 0x3b, 0x58, 0x08, byte(opcode.SYSCALL), 0, 0, 0, 0, byte(opcode.PUSHT)}
binary.LittleEndian.PutUint32(verifScript[6:], interopnames.ToID([]byte(interopnames.SystemRuntimeBurnGas)))
tx := &transaction.Transaction{
Script: []byte{byte(opcode.RET)},
Signers: []transaction.Signer{{Account: hash.Hash160(verifScript)}},
Scripts: []transaction.Witness{{
InvocationScript: []byte{byte(opcode.NOP)},
VerificationScript: verifScript,
}},
}
checkCalc(t, tx, 140065570)
})
}) })
t.Run("sendrawtransaction", func(t *testing.T) { t.Run("sendrawtransaction", func(t *testing.T) {
rpc := `{"jsonrpc": "2.0", "id": 1, "method": "sendrawtransaction", "params": ["%s"]}` rpc := `{"jsonrpc": "2.0", "id": 1, "method": "sendrawtransaction", "params": ["%s"]}`