rpcsrv: support Map parameter in invokefunction RPC handler

We have smartcontract.ParameterPair structure that can be properly
marshalled and passed to RPC server as an element of smartcontract.Map
structure. However, RPC server can't unmarshal map values properly
without this change.

This change is compatible with C# node.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
This commit is contained in:
Anna Shaleva 2024-02-27 19:56:59 +03:00
parent cc38221d77
commit 132531fabe
4 changed files with 194 additions and 68 deletions

View file

@ -34,6 +34,13 @@ type (
Type smartcontract.ParamType `json:"type"` Type smartcontract.ParamType `json:"type"`
Value Param `json:"value"` Value Param `json:"value"`
} }
// FuncParamKV represents a pair of function argument parameters
// a slice of which is stored in FuncParam of [smartcontract.MapType] type.
FuncParamKV struct {
Key FuncParam `json:"key"`
Value FuncParam `json:"value"`
}
) )
var ( var (
@ -358,6 +365,21 @@ func (p *Param) GetFuncParam() (FuncParam, error) {
return fp, err return fp, err
} }
// GetFuncParamPair returns a pair of function call parameters.
func (p *Param) GetFuncParamPair() (FuncParamKV, error) {
if p == nil {
return FuncParamKV{}, errMissingParameter
}
// This one doesn't need to be cached, it's used only once.
fpp := FuncParamKV{}
err := json.Unmarshal(p.RawMessage, &fpp)
if err != nil {
return FuncParamKV{}, err
}
return fpp, nil
}
// GetBytesHex returns a []byte value of the parameter if // GetBytesHex returns a []byte value of the parameter if
// it is a hex-encoded string. // it is a hex-encoded string.
func (p *Param) GetBytesHex() ([]byte, error) { func (p *Param) GetBytesHex() ([]byte, error) {

View file

@ -426,6 +426,48 @@ func TestParamGetFuncParam(t *testing.T) {
require.NotNil(t, err) require.NotNil(t, err)
} }
func TestParamGetFuncParamPair(t *testing.T) {
fp := FuncParam{
Type: smartcontract.MapType,
Value: Param{RawMessage: []byte(`[{"key": {"type": "String", "value": "key1"}, "value": {"type": "Integer", "value": 123}}, {"key": {"type": "String", "value": "key2"}, "value": {"type": "Integer", "value": 456}}]`)},
}
p := Param{RawMessage: []byte(`{"type": "Map", "value": [{"key": {"type": "String", "value": "key1"}, "value": {"type": "Integer", "value": 123}}, {"key": {"type": "String", "value": "key2"}, "value": {"type": "Integer", "value": 456}}]}`)}
newfp, err := p.GetFuncParam()
assert.Equal(t, fp, newfp)
require.NoError(t, err)
kvs, err := newfp.Value.GetArray()
require.NoError(t, err)
p1, err := kvs[0].GetFuncParamPair()
require.NoError(t, err)
fp1Key := FuncParam{
Type: smartcontract.StringType,
Value: Param{RawMessage: []byte(`"key1"`)},
}
fp1Value := FuncParam{
Type: smartcontract.IntegerType,
Value: Param{RawMessage: []byte(`123`)},
}
assert.Equal(t, fp1Key, p1.Key)
assert.Equal(t, fp1Value, p1.Value)
p2, err := kvs[1].GetFuncParamPair()
require.NoError(t, err)
fp2Key := FuncParam{
Type: smartcontract.StringType,
Value: Param{RawMessage: []byte(`"key2"`)},
}
fp2Value := FuncParam{
Type: smartcontract.IntegerType,
Value: Param{RawMessage: []byte(`456`)},
}
assert.Equal(t, fp2Key, p2.Key)
assert.Equal(t, fp2Value, p2.Value)
}
func TestParamGetBytesHex(t *testing.T) { func TestParamGetBytesHex(t *testing.T) {
in := "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7" in := "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7"
inb, _ := hex.DecodeString(in) inb, _ := hex.DecodeString(in)

View file

@ -13,14 +13,9 @@ import (
"github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/opcode"
) )
// ExpandArrayIntoScript pushes all FuncParam parameters from the given array // ExpandFuncParameterIntoScript pushes provided FuncParam parameter
// into the given buffer in the reverse order. // into the given buffer.
func ExpandArrayIntoScript(script *io.BinWriter, slice []Param) error { func ExpandFuncParameterIntoScript(script *io.BinWriter, fp FuncParam) error {
for j := len(slice) - 1; j >= 0; j-- {
fp, err := slice[j].GetFuncParam()
if err != nil {
return err
}
switch fp.Type { switch fp.Type {
case smartcontract.ByteArrayType: case smartcontract.ByteArrayType:
str, err := fp.Value.GetBytesBase64() str, err := fp.Value.GetBytesBase64()
@ -83,6 +78,15 @@ func ExpandArrayIntoScript(script *io.BinWriter, slice []Param) error {
if err != nil { if err != nil {
return err return err
} }
case smartcontract.MapType:
val, err := fp.Value.GetArray()
if err != nil {
return err
}
err = ExpandMapIntoScriptAndPack(script, val)
if err != nil {
return err
}
case smartcontract.AnyType: case smartcontract.AnyType:
if fp.Value.IsNull() || len(fp.Value.RawMessage) == 0 { if fp.Value.IsNull() || len(fp.Value.RawMessage) == 0 {
emit.Opcodes(script, opcode.PUSHNULL) emit.Opcodes(script, opcode.PUSHNULL)
@ -90,6 +94,21 @@ func ExpandArrayIntoScript(script *io.BinWriter, slice []Param) error {
default: default:
return fmt.Errorf("parameter type %v is not supported", fp.Type) return fmt.Errorf("parameter type %v is not supported", fp.Type)
} }
return script.Err
}
// ExpandArrayIntoScript pushes all FuncParam parameters from the given array
// into the given buffer in the reverse order.
func ExpandArrayIntoScript(script *io.BinWriter, slice []Param) error {
for j := len(slice) - 1; j >= 0; j-- {
fp, err := slice[j].GetFuncParam()
if err != nil {
return err
}
err = ExpandFuncParameterIntoScript(script, fp)
if err != nil {
return fmt.Errorf("param %d: %w", j, err)
}
} }
return script.Err return script.Err
} }
@ -110,6 +129,32 @@ func ExpandArrayIntoScriptAndPack(script *io.BinWriter, slice []Param) error {
return script.Err return script.Err
} }
// ExpandMapIntoScriptAndPack expands provided array of key-value items into script
// and packs the resulting pairs in the [stackitem.Map].
func ExpandMapIntoScriptAndPack(script *io.BinWriter, slice []Param) error {
if len(slice) == 0 {
emit.Opcodes(script, opcode.NEWMAP)
return script.Err
}
for i := len(slice) - 1; i >= 0; i-- {
pair, err := slice[i].GetFuncParamPair()
if err != nil {
return err
}
err = ExpandFuncParameterIntoScript(script, pair.Value)
if err != nil {
return fmt.Errorf("map value %d: %w", i, err)
}
err = ExpandFuncParameterIntoScript(script, pair.Key)
if err != nil {
return fmt.Errorf("map key %d: %w", i, err)
}
}
emit.Int(script, int64(len(slice)))
emit.Opcodes(script, opcode.PACKMAP)
return script.Err
}
// CreateFunctionInvocationScript creates a script to invoke the given contract with // CreateFunctionInvocationScript creates a script to invoke the given contract with
// the given parameters. // the given parameters.
func CreateFunctionInvocationScript(contract util.Uint160, method string, param *Param) ([]byte, error) { func CreateFunctionInvocationScript(contract util.Uint160, method string, param *Param) ([]byte, error) {

View file

@ -124,6 +124,17 @@ func TestExpandArrayIntoScript(t *testing.T) {
Input: []Param{{RawMessage: []byte(`{"type": "Integer", "value": "` + bi.String() + `"}`)}}, Input: []Param{{RawMessage: []byte(`{"type": "Integer", "value": "` + bi.String() + `"}`)}},
Expected: append([]byte{byte(opcode.PUSHINT256)}, rawInt...), Expected: append([]byte{byte(opcode.PUSHINT256)}, rawInt...),
}, },
{
Input: []Param{{RawMessage: []byte(`{"type": "Map", "value": [{"key": {"type": "String", "value": "a" }, "value": {"type": "Integer", "value": 1}}, {"key": {"type": "String", "value": "b"}, "value": {"type": "Integer", "value": 2}}]}`)}},
Expected: []byte{
byte(opcode.PUSH2), // value of element #2
byte(opcode.PUSHDATA1), 1, byte('b'), // key of element #2
byte(opcode.PUSH1), // value of element #1
byte(opcode.PUSHDATA1), 1, byte('a'), // key of element #1
byte(opcode.PUSH2), // map len
byte(opcode.PACKMAP),
},
},
} }
for _, c := range testCases { for _, c := range testCases {
script := io.NewBufBinWriter() script := io.NewBufBinWriter()
@ -142,6 +153,12 @@ func TestExpandArrayIntoScript(t *testing.T) {
{RawMessage: []byte(`{"type": "Integer", "value": "` + {RawMessage: []byte(`{"type": "Integer", "value": "` +
new(big.Int).Lsh(big.NewInt(1), 255).String() + `"}`)}, new(big.Int).Lsh(big.NewInt(1), 255).String() + `"}`)},
}, },
{
{RawMessage: []byte(`{"type": "Map", "value": [{"key": {"type": "InvalidT", "value": "a" }, "value": {"type": "Integer", "value": 1}}]}`)},
},
{
{RawMessage: []byte(`{"type": "Map", "value": [{"key": {"type": "String", "value": "a" }, "value": {"type": "Integer", "value": "not-an-int"}}]}`)},
},
} }
for _, c := range errorCases { for _, c := range errorCases {
script := io.NewBufBinWriter() script := io.NewBufBinWriter()