From 132531fabe9b698e97870ab16a6ef4c271333f63 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 27 Feb 2024 19:56:59 +0300 Subject: [PATCH] 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 --- pkg/services/rpcsrv/params/param.go | 22 +++ pkg/services/rpcsrv/params/param_test.go | 42 ++++ pkg/services/rpcsrv/params/txBuilder.go | 181 +++++++++++------- pkg/services/rpcsrv/params/tx_builder_test.go | 17 ++ 4 files changed, 194 insertions(+), 68 deletions(-) diff --git a/pkg/services/rpcsrv/params/param.go b/pkg/services/rpcsrv/params/param.go index 3e07f22f4..167af48d6 100644 --- a/pkg/services/rpcsrv/params/param.go +++ b/pkg/services/rpcsrv/params/param.go @@ -34,6 +34,13 @@ type ( Type smartcontract.ParamType `json:"type"` 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 ( @@ -358,6 +365,21 @@ func (p *Param) GetFuncParam() (FuncParam, error) { 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 // it is a hex-encoded string. func (p *Param) GetBytesHex() ([]byte, error) { diff --git a/pkg/services/rpcsrv/params/param_test.go b/pkg/services/rpcsrv/params/param_test.go index d7d866e53..ff1a701d3 100644 --- a/pkg/services/rpcsrv/params/param_test.go +++ b/pkg/services/rpcsrv/params/param_test.go @@ -426,6 +426,48 @@ func TestParamGetFuncParam(t *testing.T) { 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) { in := "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7" inb, _ := hex.DecodeString(in) diff --git a/pkg/services/rpcsrv/params/txBuilder.go b/pkg/services/rpcsrv/params/txBuilder.go index 9d2c9ea2d..29db92533 100644 --- a/pkg/services/rpcsrv/params/txBuilder.go +++ b/pkg/services/rpcsrv/params/txBuilder.go @@ -13,6 +13,90 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm/opcode" ) +// ExpandFuncParameterIntoScript pushes provided FuncParam parameter +// into the given buffer. +func ExpandFuncParameterIntoScript(script *io.BinWriter, fp FuncParam) error { + switch fp.Type { + case smartcontract.ByteArrayType: + str, err := fp.Value.GetBytesBase64() + if err != nil { + return err + } + emit.Bytes(script, str) + case smartcontract.SignatureType: + str, err := fp.Value.GetBytesBase64() + if err != nil { + return err + } + emit.Bytes(script, str) + case smartcontract.StringType: + str, err := fp.Value.GetString() + if err != nil { + return err + } + emit.String(script, str) + case smartcontract.Hash160Type: + hash, err := fp.Value.GetUint160FromHex() + if err != nil { + return err + } + emit.Bytes(script, hash.BytesBE()) + case smartcontract.Hash256Type: + hash, err := fp.Value.GetUint256() + if err != nil { + return err + } + emit.Bytes(script, hash.BytesBE()) + case smartcontract.PublicKeyType: + str, err := fp.Value.GetString() + if err != nil { + return err + } + key, err := keys.NewPublicKeyFromString(string(str)) + if err != nil { + return err + } + emit.Bytes(script, key.Bytes()) + case smartcontract.IntegerType: + bi, err := fp.Value.GetBigInt() + if err != nil { + return err + } + emit.BigInt(script, bi) + case smartcontract.BoolType: + val, err := fp.Value.GetBoolean() // not GetBooleanStrict(), because that's the way C# code works + if err != nil { + return errors.New("not a bool") + } + emit.Bool(script, val) + case smartcontract.ArrayType: + val, err := fp.Value.GetArray() + if err != nil { + return err + } + err = ExpandArrayIntoScriptAndPack(script, val) + if err != nil { + 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: + if fp.Value.IsNull() || len(fp.Value.RawMessage) == 0 { + emit.Opcodes(script, opcode.PUSHNULL) + } + default: + 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 { @@ -21,74 +105,9 @@ func ExpandArrayIntoScript(script *io.BinWriter, slice []Param) error { if err != nil { return err } - switch fp.Type { - case smartcontract.ByteArrayType: - str, err := fp.Value.GetBytesBase64() - if err != nil { - return err - } - emit.Bytes(script, str) - case smartcontract.SignatureType: - str, err := fp.Value.GetBytesBase64() - if err != nil { - return err - } - emit.Bytes(script, str) - case smartcontract.StringType: - str, err := fp.Value.GetString() - if err != nil { - return err - } - emit.String(script, str) - case smartcontract.Hash160Type: - hash, err := fp.Value.GetUint160FromHex() - if err != nil { - return err - } - emit.Bytes(script, hash.BytesBE()) - case smartcontract.Hash256Type: - hash, err := fp.Value.GetUint256() - if err != nil { - return err - } - emit.Bytes(script, hash.BytesBE()) - case smartcontract.PublicKeyType: - str, err := fp.Value.GetString() - if err != nil { - return err - } - key, err := keys.NewPublicKeyFromString(string(str)) - if err != nil { - return err - } - emit.Bytes(script, key.Bytes()) - case smartcontract.IntegerType: - bi, err := fp.Value.GetBigInt() - if err != nil { - return err - } - emit.BigInt(script, bi) - case smartcontract.BoolType: - val, err := fp.Value.GetBoolean() // not GetBooleanStrict(), because that's the way C# code works - if err != nil { - return errors.New("not a bool") - } - emit.Bool(script, val) - case smartcontract.ArrayType: - val, err := fp.Value.GetArray() - if err != nil { - return err - } - err = ExpandArrayIntoScriptAndPack(script, val) - if err != nil { - return err - } - case smartcontract.AnyType: - if fp.Value.IsNull() || len(fp.Value.RawMessage) == 0 { - emit.Opcodes(script, opcode.PUSHNULL) - } - default: - return fmt.Errorf("parameter type %v is not supported", fp.Type) + err = ExpandFuncParameterIntoScript(script, fp) + if err != nil { + return fmt.Errorf("param %d: %w", j, err) } } return script.Err @@ -110,6 +129,32 @@ func ExpandArrayIntoScriptAndPack(script *io.BinWriter, slice []Param) error { 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 // the given parameters. func CreateFunctionInvocationScript(contract util.Uint160, method string, param *Param) ([]byte, error) { diff --git a/pkg/services/rpcsrv/params/tx_builder_test.go b/pkg/services/rpcsrv/params/tx_builder_test.go index ed495b21b..8df559817 100644 --- a/pkg/services/rpcsrv/params/tx_builder_test.go +++ b/pkg/services/rpcsrv/params/tx_builder_test.go @@ -124,6 +124,17 @@ func TestExpandArrayIntoScript(t *testing.T) { Input: []Param{{RawMessage: []byte(`{"type": "Integer", "value": "` + bi.String() + `"}`)}}, 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 { script := io.NewBufBinWriter() @@ -142,6 +153,12 @@ func TestExpandArrayIntoScript(t *testing.T) { {RawMessage: []byte(`{"type": "Integer", "value": "` + 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 { script := io.NewBufBinWriter()