compiler: enforce runtime.Notify parameters cast

If notification parameters type can be defined in a compile time then enforce
parameter cast to the desired type got from manifest.
This commit is contained in:
Anna Shaleva 2022-09-29 16:53:28 +03:00
parent 80f71a4e6e
commit 554e48e7b7
5 changed files with 266 additions and 13 deletions

View file

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"math/big"
"strconv"
"strings"
"testing"
@ -22,6 +23,7 @@ import (
cinterop "github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/neotest"
"github.com/nspcc-dev/neo-go/pkg/neotest/chain"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
@ -548,3 +550,145 @@ func TestCallWithVersion(t *testing.T) {
c.InvokeFail(t, "contract version mismatch", "callWithVersion", policyH.BytesBE(), 1, "getExecFeeFactor")
})
}
func TestForcedNotifyArgumentsConversion(t *testing.T) {
const methodWithEllipsis = "withEllipsis"
const methodWithoutEllipsis = "withoutEllipsis"
check := func(t *testing.T, method string, targetSCParamTypes []smartcontract.ParamType, expectedVMParamTypes []stackitem.Type, noEventsCheck bool) {
bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc)
src := `package foo
import "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
const arg4 = 4 // Const value.
func WithoutEllipsis() {
var arg0 int // Default value.
var arg1 int = 1 // Initialized value.
arg2 := 2 // Short decl.
var arg3 int
arg3 = 3 // Declare first, change value afterwards.
runtime.Notify("withoutEllipsis", arg0, arg1, arg2, arg3, arg4, 5, f(6)) // The fifth argument is basic literal.
}
func WithEllipsis() {
arg := []interface{}{0, 1, f(2), 3, 4, 5, 6}
runtime.Notify("withEllipsis", arg...)
}
func f(i int) int {
return i
}`
count := len(targetSCParamTypes)
if count != len(expectedVMParamTypes) {
t.Fatalf("parameters count mismatch: %d vs %d", count, len(expectedVMParamTypes))
}
scParams := make([]manifest.Parameter, len(targetSCParamTypes))
vmParams := make([]stackitem.Item, len(expectedVMParamTypes))
for i := range scParams {
scParams[i] = manifest.Parameter{
Name: strconv.Itoa(i),
Type: targetSCParamTypes[i],
}
defaultValue := stackitem.NewBigInteger(big.NewInt(int64(i)))
var (
val stackitem.Item
err error
)
if expectedVMParamTypes[i] == stackitem.IntegerT {
val = defaultValue
} else {
val, err = defaultValue.Convert(expectedVMParamTypes[i]) // exactly the same conversion should be emitted by compiler and performed by the contract code.
require.NoError(t, err)
}
vmParams[i] = val
}
ctr := neotest.CompileSource(t, e.CommitteeHash, strings.NewReader(src), &compiler.Options{
Name: "Helper",
ContractEvents: []manifest.Event{
{
Name: methodWithoutEllipsis,
Parameters: scParams,
},
{
Name: methodWithEllipsis,
Parameters: scParams,
},
},
NoEventsCheck: noEventsCheck,
})
e.DeployContract(t, ctr, nil)
c := e.CommitteeInvoker(ctr.Hash)
t.Run(method, func(t *testing.T) {
h := c.Invoke(t, stackitem.Null{}, method)
aer := c.GetTxExecResult(t, h)
require.Equal(t, 1, len(aer.Events))
require.Equal(t, stackitem.NewArray(vmParams), aer.Events[0].Item)
})
}
checkSingleType := func(t *testing.T, method string, targetSCEventType smartcontract.ParamType, expectedVMType stackitem.Type, noEventsCheck ...bool) {
count := 7
scParams := make([]smartcontract.ParamType, count)
vmParams := make([]stackitem.Type, count)
for i := range scParams {
scParams[i] = targetSCEventType
vmParams[i] = expectedVMType
}
var noEvents bool
if len(noEventsCheck) > 0 {
noEvents = noEventsCheck[0]
}
check(t, method, scParams, vmParams, noEvents)
}
t.Run("good, single type, default values", func(t *testing.T) {
checkSingleType(t, methodWithoutEllipsis, smartcontract.IntegerType, stackitem.IntegerT)
})
t.Run("good, single type, conversion to BooleanT", func(t *testing.T) {
checkSingleType(t, methodWithoutEllipsis, smartcontract.BoolType, stackitem.BooleanT)
})
t.Run("good, single type, Hash160Type->ByteArray", func(t *testing.T) {
checkSingleType(t, methodWithoutEllipsis, smartcontract.Hash160Type, stackitem.ByteArrayT)
})
t.Run("good, single type, Hash256Type->ByteArray", func(t *testing.T) {
checkSingleType(t, methodWithoutEllipsis, smartcontract.Hash256Type, stackitem.ByteArrayT)
})
t.Run("good, single type, Signature->ByteArray", func(t *testing.T) {
checkSingleType(t, methodWithoutEllipsis, smartcontract.SignatureType, stackitem.ByteArrayT)
})
t.Run("good, single type, String->ByteArray", func(t *testing.T) {
checkSingleType(t, methodWithoutEllipsis, smartcontract.StringType, stackitem.ByteArrayT) // Special case, runtime.Notify will convert any Buffer to ByteArray.
})
t.Run("good, single type, PublicKeyType->ByteArray", func(t *testing.T) {
checkSingleType(t, methodWithoutEllipsis, smartcontract.PublicKeyType, stackitem.ByteArrayT)
})
t.Run("good, single type, AnyType->do not change initial type", func(t *testing.T) {
checkSingleType(t, methodWithoutEllipsis, smartcontract.AnyType, stackitem.IntegerT) // Special case, compiler should leave the type "as is" and do not emit conversion code.
})
// Test for InteropInterface->... is missing, because we don't enforce conversion to stackitem.InteropInterface,
// but compiler still checks these notifications against expected manifest.
t.Run("good, multiple types, check the conversion order", func(t *testing.T) {
check(t, methodWithoutEllipsis, []smartcontract.ParamType{
smartcontract.IntegerType,
smartcontract.BoolType,
smartcontract.ByteArrayType,
smartcontract.PublicKeyType,
smartcontract.Hash160Type,
smartcontract.AnyType, // leave initial type
smartcontract.StringType,
}, []stackitem.Type{
stackitem.IntegerT,
stackitem.BooleanT,
stackitem.ByteArrayT,
stackitem.ByteArrayT,
stackitem.ByteArrayT,
stackitem.IntegerT, // leave initial type
stackitem.ByteArrayT,
}, false)
})
t.Run("with ellipsis, do not emit conversion code", func(t *testing.T) {
checkSingleType(t, methodWithEllipsis, smartcontract.IntegerType, stackitem.IntegerT)
checkSingleType(t, methodWithEllipsis, smartcontract.BoolType, stackitem.IntegerT)
checkSingleType(t, methodWithEllipsis, smartcontract.ByteArrayType, stackitem.IntegerT)
})
t.Run("no events check => no conversion code", func(t *testing.T) {
checkSingleType(t, methodWithoutEllipsis, smartcontract.PublicKeyType, stackitem.IntegerT, true)
})
}