rpc/request: delay parameter unmarshaling

It is rather costly to try to unmarshal many structs in order.

Signed-off-by: Evgeniy Stratonikov <evgeniy@nspcc.ru>
This commit is contained in:
Evgeniy Stratonikov 2021-08-05 17:56:17 +03:00
parent 5aff82aef4
commit 3c34e6fa21
4 changed files with 197 additions and 202 deletions

View file

@ -2,6 +2,7 @@ package client
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -186,10 +187,10 @@ func TestWSFilteredSubscriptions(t *testing.T) {
}, },
func(t *testing.T, p *request.Params) { func(t *testing.T, p *request.Params) {
param := p.Value(1) param := p.Value(1)
require.NotNil(t, param) raw, ok := param.Value.(json.RawMessage)
require.Equal(t, request.BlockFilterT, param.Type) require.True(t, ok)
filt, ok := param.Value.(request.BlockFilter) filt := new(request.BlockFilter)
require.Equal(t, true, ok) require.NoError(t, json.Unmarshal(raw, filt))
require.Equal(t, 3, filt.Primary) require.Equal(t, 3, filt.Primary)
}, },
}, },
@ -201,10 +202,10 @@ func TestWSFilteredSubscriptions(t *testing.T) {
}, },
func(t *testing.T, p *request.Params) { func(t *testing.T, p *request.Params) {
param := p.Value(1) param := p.Value(1)
require.NotNil(t, param) raw, ok := param.Value.(json.RawMessage)
require.Equal(t, request.TxFilterT, param.Type) require.True(t, ok)
filt, ok := param.Value.(request.TxFilter) filt := new(request.TxFilter)
require.Equal(t, true, ok) require.NoError(t, json.Unmarshal(raw, filt))
require.Equal(t, util.Uint160{1, 2, 3, 4, 5}, *filt.Sender) require.Equal(t, util.Uint160{1, 2, 3, 4, 5}, *filt.Sender)
require.Nil(t, filt.Signer) require.Nil(t, filt.Signer)
}, },
@ -217,10 +218,10 @@ func TestWSFilteredSubscriptions(t *testing.T) {
}, },
func(t *testing.T, p *request.Params) { func(t *testing.T, p *request.Params) {
param := p.Value(1) param := p.Value(1)
require.NotNil(t, param) raw, ok := param.Value.(json.RawMessage)
require.Equal(t, request.TxFilterT, param.Type) require.True(t, ok)
filt, ok := param.Value.(request.TxFilter) filt := new(request.TxFilter)
require.Equal(t, true, ok) require.NoError(t, json.Unmarshal(raw, filt))
require.Nil(t, filt.Sender) require.Nil(t, filt.Sender)
require.Equal(t, util.Uint160{0, 42}, *filt.Signer) require.Equal(t, util.Uint160{0, 42}, *filt.Signer)
}, },
@ -234,10 +235,10 @@ func TestWSFilteredSubscriptions(t *testing.T) {
}, },
func(t *testing.T, p *request.Params) { func(t *testing.T, p *request.Params) {
param := p.Value(1) param := p.Value(1)
require.NotNil(t, param) raw, ok := param.Value.(json.RawMessage)
require.Equal(t, request.TxFilterT, param.Type) require.True(t, ok)
filt, ok := param.Value.(request.TxFilter) filt := new(request.TxFilter)
require.Equal(t, true, ok) require.NoError(t, json.Unmarshal(raw, filt))
require.Equal(t, util.Uint160{1, 2, 3, 4, 5}, *filt.Sender) require.Equal(t, util.Uint160{1, 2, 3, 4, 5}, *filt.Sender)
require.Equal(t, util.Uint160{0, 42}, *filt.Signer) require.Equal(t, util.Uint160{0, 42}, *filt.Signer)
}, },
@ -250,10 +251,10 @@ func TestWSFilteredSubscriptions(t *testing.T) {
}, },
func(t *testing.T, p *request.Params) { func(t *testing.T, p *request.Params) {
param := p.Value(1) param := p.Value(1)
require.NotNil(t, param) raw, ok := param.Value.(json.RawMessage)
require.Equal(t, request.NotificationFilterT, param.Type) require.True(t, ok)
filt, ok := param.Value.(request.NotificationFilter) filt := new(request.NotificationFilter)
require.Equal(t, true, ok) require.NoError(t, json.Unmarshal(raw, filt))
require.Equal(t, util.Uint160{1, 2, 3, 4, 5}, *filt.Contract) require.Equal(t, util.Uint160{1, 2, 3, 4, 5}, *filt.Contract)
require.Nil(t, filt.Name) require.Nil(t, filt.Name)
}, },
@ -266,10 +267,10 @@ func TestWSFilteredSubscriptions(t *testing.T) {
}, },
func(t *testing.T, p *request.Params) { func(t *testing.T, p *request.Params) {
param := p.Value(1) param := p.Value(1)
require.NotNil(t, param) raw, ok := param.Value.(json.RawMessage)
require.Equal(t, request.NotificationFilterT, param.Type) require.True(t, ok)
filt, ok := param.Value.(request.NotificationFilter) filt := new(request.NotificationFilter)
require.Equal(t, true, ok) require.NoError(t, json.Unmarshal(raw, filt))
require.Equal(t, "my_pretty_notification", *filt.Name) require.Equal(t, "my_pretty_notification", *filt.Name)
require.Nil(t, filt.Contract) require.Nil(t, filt.Contract)
}, },
@ -283,10 +284,10 @@ func TestWSFilteredSubscriptions(t *testing.T) {
}, },
func(t *testing.T, p *request.Params) { func(t *testing.T, p *request.Params) {
param := p.Value(1) param := p.Value(1)
require.NotNil(t, param) raw, ok := param.Value.(json.RawMessage)
require.Equal(t, request.NotificationFilterT, param.Type) require.True(t, ok)
filt, ok := param.Value.(request.NotificationFilter) filt := new(request.NotificationFilter)
require.Equal(t, true, ok) require.NoError(t, json.Unmarshal(raw, filt))
require.Equal(t, util.Uint160{1, 2, 3, 4, 5}, *filt.Contract) require.Equal(t, util.Uint160{1, 2, 3, 4, 5}, *filt.Contract)
require.Equal(t, "my_pretty_notification", *filt.Name) require.Equal(t, "my_pretty_notification", *filt.Name)
}, },
@ -299,10 +300,10 @@ func TestWSFilteredSubscriptions(t *testing.T) {
}, },
func(t *testing.T, p *request.Params) { func(t *testing.T, p *request.Params) {
param := p.Value(1) param := p.Value(1)
require.NotNil(t, param) raw, ok := param.Value.(json.RawMessage)
require.Equal(t, request.ExecutionFilterT, param.Type) require.True(t, ok)
filt, ok := param.Value.(request.ExecutionFilter) filt := new(request.ExecutionFilter)
require.Equal(t, true, ok) require.NoError(t, json.Unmarshal(raw, filt))
require.Equal(t, "FAULT", filt.State) require.Equal(t, "FAULT", filt.State)
}, },
}, },

View file

@ -134,9 +134,20 @@ func (p *Param) GetArray() ([]Param, error) {
return nil, errMissingParameter return nil, errMissingParameter
} }
a, ok := p.Value.([]Param) a, ok := p.Value.([]Param)
if ok {
return a, nil
}
raw, ok := p.Value.(json.RawMessage)
if !ok { if !ok {
return nil, errors.New("not an array") return nil, errors.New("not an array")
} }
a = []Param{}
err := json.Unmarshal(raw, &a)
if err != nil {
return nil, errors.New("not an array")
}
p.Value = a
return a, nil return a, nil
} }
@ -190,9 +201,18 @@ func (p *Param) GetFuncParam() (FuncParam, error) {
return FuncParam{}, errMissingParameter return FuncParam{}, errMissingParameter
} }
fp, ok := p.Value.(FuncParam) fp, ok := p.Value.(FuncParam)
if ok {
return fp, nil
}
raw, ok := p.Value.(json.RawMessage)
if !ok { if !ok {
return FuncParam{}, errors.New("not a function parameter") return FuncParam{}, errors.New("not a function parameter")
} }
err := json.Unmarshal(raw, &fp)
if err != nil {
return fp, err
}
p.Value = fp
return fp, nil return fp, nil
} }
@ -219,11 +239,38 @@ func (p *Param) GetBytesBase64() ([]byte, error) {
} }
// GetSignerWithWitness returns SignerWithWitness value of the parameter. // GetSignerWithWitness returns SignerWithWitness value of the parameter.
func (p Param) GetSignerWithWitness() (SignerWithWitness, error) { func (p *Param) GetSignerWithWitness() (SignerWithWitness, error) {
c, ok := p.Value.(SignerWithWitness) c, ok := p.Value.(SignerWithWitness)
if ok {
return c, nil
}
raw, ok := p.Value.(json.RawMessage)
if !ok { if !ok {
return SignerWithWitness{}, errors.New("not a signer") return SignerWithWitness{}, errors.New("not a signer")
} }
aux := new(signerWithWitnessAux)
err := json.Unmarshal(raw, aux)
if err != nil {
return SignerWithWitness{}, errors.New("not a signer")
}
accParam := Param{StringT, aux.Account}
acc, err := accParam.GetUint160FromAddressOrHex()
if err != nil {
return SignerWithWitness{}, errors.New("not a signer")
}
c = SignerWithWitness{
Signer: transaction.Signer{
Account: acc,
Scopes: aux.Scopes,
AllowedContracts: aux.AllowedContracts,
AllowedGroups: aux.AllowedGroups,
},
Witness: transaction.Witness{
InvocationScript: aux.InvocationScript,
VerificationScript: aux.VerificationScript,
},
}
p.Value = c
return c, nil return c, nil
} }
@ -264,84 +311,47 @@ func (p Param) GetSignersWithWitnesses() ([]transaction.Signer, []transaction.Wi
// UnmarshalJSON implements json.Unmarshaler interface. // UnmarshalJSON implements json.Unmarshaler interface.
func (p *Param) UnmarshalJSON(data []byte) error { func (p *Param) UnmarshalJSON(data []byte) error {
var s string
var num float64
var b bool
// To unmarshal correctly we need to pass pointers into the decoder.
var attempts = [...]Param{
{NumberT, &num},
{BooleanT, &b},
{StringT, &s},
{FuncParamT, &FuncParam{}},
{BlockFilterT, &BlockFilter{}},
{TxFilterT, &TxFilter{}},
{NotificationFilterT, &NotificationFilter{}},
{ExecutionFilterT, &ExecutionFilter{}},
{SignerWithWitnessT, &signerWithWitnessAux{}},
{ArrayT, &[]Param{}},
}
if bytes.Equal(data, []byte("null")) {
p.Type = defaultT
return nil
}
for _, cur := range attempts {
r := bytes.NewReader(data) r := bytes.NewReader(data)
jd := json.NewDecoder(r) jd := json.NewDecoder(r)
jd.DisallowUnknownFields() jd.UseNumber()
if err := jd.Decode(cur.Value); err == nil { tok, err := jd.Token()
p.Type = cur.Type
// But we need to store actual values, not pointers.
switch val := cur.Value.(type) {
case *float64:
p.Value = int(*val)
case *string:
p.Value = *val
case *bool:
p.Value = *val
case *FuncParam:
p.Value = *val
case *BlockFilter:
p.Value = *val
case *TxFilter:
p.Value = *val
case *NotificationFilter:
p.Value = *val
case *ExecutionFilter:
if (*val).State == "HALT" || (*val).State == "FAULT" {
p.Value = *val
} else {
continue
}
case *signerWithWitnessAux:
aux := *val
accParam := Param{StringT, aux.Account}
acc, err := accParam.GetUint160FromAddressOrHex()
if err != nil { if err != nil {
return err return err
} }
p.Value = SignerWithWitness{ switch t := tok.(type) {
Signer: transaction.Signer{ case json.Delim:
Account: acc, if t == json.Delim('[') {
Scopes: aux.Scopes, var arr []Param
AllowedContracts: aux.AllowedContracts, err := json.Unmarshal(data, &arr)
AllowedGroups: aux.AllowedGroups, if err != nil {
}, return err
Witness: transaction.Witness{
InvocationScript: aux.InvocationScript,
VerificationScript: aux.VerificationScript,
},
} }
case *[]Param: p.Type = ArrayT
p.Value = *val p.Value = arr
} else {
p.Type = defaultT
p.Value = json.RawMessage(data)
}
case bool:
p.Type = BooleanT
p.Value = t
case float64: // unexpected because of `UseNumber`.
panic("unexpected")
case json.Number:
value, err := strconv.Atoi(string(t))
if err != nil {
return err
}
p.Type = NumberT
p.Value = value
case string:
p.Type = StringT
p.Value = t
default: // null
p.Type = defaultT
} }
return nil return nil
} }
}
return errors.New("unknown type")
}
// signerWithWitnessAux is an auxiluary struct for JSON marshalling. We need it because of // signerWithWitnessAux is an auxiluary struct for JSON marshalling. We need it because of
// DisallowUnknownFields JSON marshaller setting. // DisallowUnknownFields JSON marshaller setting.

View file

@ -16,24 +16,9 @@ import (
func TestParam_UnmarshalJSON(t *testing.T) { func TestParam_UnmarshalJSON(t *testing.T) {
msg := `["str1", 123, null, ["str2", 3], [{"type": "String", "value": "jajaja"}], msg := `["str1", 123, null, ["str2", 3], [{"type": "String", "value": "jajaja"}],
{"primary": 1},
{"sender": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"},
{"signer": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"},
{"sender": "f84d6a337fbc3d3a201d41da99e86b479e7a2554", "signer": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"},
{"contract": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"},
{"name": "my_pretty_notification"},
{"contract": "f84d6a337fbc3d3a201d41da99e86b479e7a2554", "name":"my_pretty_notification"},
{"state": "HALT"},
{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569"}, {"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569"},
{"account": "NYxb4fSZVKAz8YsgaPK2WkT3KcAE9b3Vag", "scopes": "Global"}, {"account": "NYxb4fSZVKAz8YsgaPK2WkT3KcAE9b3Vag", "scopes": "Global"},
[{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": "Global"}]]` [{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": "Global"}]]`
contr, err := util.Uint160DecodeStringLE("f84d6a337fbc3d3a201d41da99e86b479e7a2554")
require.NoError(t, err)
name := "my_pretty_notification"
accountHash, err := util.Uint160DecodeStringLE("cadb3dc2faa3ef14a13b619c9a43124755aa2569")
require.NoError(t, err)
addrHash, err := address.StringToUint160("NYxb4fSZVKAz8YsgaPK2WkT3KcAE9b3Vag")
require.NoError(t, err)
expected := Params{ expected := Params{
{ {
Type: StringT, Type: StringT,
@ -63,78 +48,25 @@ func TestParam_UnmarshalJSON(t *testing.T) {
Type: ArrayT, Type: ArrayT,
Value: []Param{ Value: []Param{
{ {
Type: FuncParamT, Type: defaultT,
Value: FuncParam{ Value: json.RawMessage(`{"type": "String", "value": "jajaja"}`),
Type: smartcontract.StringType,
Value: Param{
Type: StringT,
Value: "jajaja",
},
},
},
},
},
{
Type: BlockFilterT,
Value: BlockFilter{Primary: 1},
},
{
Type: TxFilterT,
Value: TxFilter{Sender: &contr},
},
{
Type: TxFilterT,
Value: TxFilter{Signer: &contr},
},
{
Type: TxFilterT,
Value: TxFilter{Sender: &contr, Signer: &contr},
},
{
Type: NotificationFilterT,
Value: NotificationFilter{Contract: &contr},
},
{
Type: NotificationFilterT,
Value: NotificationFilter{Name: &name},
},
{
Type: NotificationFilterT,
Value: NotificationFilter{Contract: &contr, Name: &name},
},
{
Type: ExecutionFilterT,
Value: ExecutionFilter{State: "HALT"},
},
{
Type: SignerWithWitnessT,
Value: SignerWithWitness{
Signer: transaction.Signer{
Account: accountHash,
Scopes: transaction.None,
}, },
}, },
}, },
{ {
Type: SignerWithWitnessT, Type: defaultT,
Value: SignerWithWitness{ Value: json.RawMessage(`{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569"}`),
Signer: transaction.Signer{
Account: addrHash,
Scopes: transaction.Global,
},
}, },
{
Type: defaultT,
Value: json.RawMessage(`{"account": "NYxb4fSZVKAz8YsgaPK2WkT3KcAE9b3Vag", "scopes": "Global"}`),
}, },
{ {
Type: ArrayT, Type: ArrayT,
Value: []Param{ Value: []Param{
{ {
Type: SignerWithWitnessT, Type: defaultT,
Value: SignerWithWitness{ Value: json.RawMessage(`{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": "Global"}`),
Signer: transaction.Signer{
Account: accountHash,
Scopes: transaction.Global,
},
},
}, },
}, },
}, },
@ -143,11 +75,49 @@ func TestParam_UnmarshalJSON(t *testing.T) {
var ps Params var ps Params
require.NoError(t, json.Unmarshal([]byte(msg), &ps)) require.NoError(t, json.Unmarshal([]byte(msg), &ps))
require.Equal(t, expected, ps) require.Equal(t, expected, ps)
}
msg = `[{"2": 3}]` func TestGetWitness(t *testing.T) {
require.Error(t, json.Unmarshal([]byte(msg), &ps)) accountHash, err := util.Uint160DecodeStringLE("cadb3dc2faa3ef14a13b619c9a43124755aa2569")
msg = `[{"account": "notanaccount", "scopes": "Global"}]` require.NoError(t, err)
require.Error(t, json.Unmarshal([]byte(msg), &ps)) addrHash, err := address.StringToUint160("NYxb4fSZVKAz8YsgaPK2WkT3KcAE9b3Vag")
require.NoError(t, err)
testCases := []struct {
raw string
expected SignerWithWitness
}{
{`{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569"}`, SignerWithWitness{
Signer: transaction.Signer{
Account: accountHash,
Scopes: transaction.None,
}},
},
{`{"account": "NYxb4fSZVKAz8YsgaPK2WkT3KcAE9b3Vag", "scopes": "Global"}`, SignerWithWitness{
Signer: transaction.Signer{
Account: addrHash,
Scopes: transaction.Global,
}},
},
{`{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": "Global"}`, SignerWithWitness{
Signer: transaction.Signer{
Account: accountHash,
Scopes: transaction.Global,
}},
},
}
for _, tc := range testCases {
p := Param{Value: json.RawMessage(tc.raw)}
actual, err := p.GetSignerWithWitness()
require.NoError(t, err)
require.Equal(t, tc.expected, actual)
require.Equal(t, tc.expected, p.Value)
actual, err = p.GetSignerWithWitness() // valid second invocation.
require.NoError(t, err)
require.Equal(t, tc.expected, actual)
}
} }
func TestParamGetString(t *testing.T) { func TestParamGetString(t *testing.T) {

View file

@ -1,6 +1,7 @@
package server package server
import ( import (
"bytes"
"context" "context"
"crypto/elliptic" "crypto/elliptic"
"encoding/binary" "encoding/binary"
@ -1496,27 +1497,40 @@ func (s *Server) subscribe(reqParams request.Params, sub *subscriber) (interface
// Optional filter. // Optional filter.
var filter interface{} var filter interface{}
if p := reqParams.Value(1); p != nil { if p := reqParams.Value(1); p != nil {
param, ok := p.Value.(json.RawMessage)
if !ok {
return nil, response.ErrInvalidParams
}
jd := json.NewDecoder(bytes.NewReader(param))
jd.DisallowUnknownFields()
switch event { switch event {
case response.BlockEventID: case response.BlockEventID:
if p.Type != request.BlockFilterT { flt := new(request.BlockFilter)
return nil, response.ErrInvalidParams err = jd.Decode(flt)
} p.Type = request.BlockFilterT
case response.TransactionEventID: p.Value = *flt
if p.Type != request.TxFilterT { case response.TransactionEventID, response.NotaryRequestEventID:
return nil, response.ErrInvalidParams flt := new(request.TxFilter)
} err = jd.Decode(flt)
p.Type = request.TxFilterT
p.Value = *flt
case response.NotificationEventID: case response.NotificationEventID:
if p.Type != request.NotificationFilterT { flt := new(request.NotificationFilter)
return nil, response.ErrInvalidParams err = jd.Decode(flt)
} p.Type = request.NotificationFilterT
p.Value = *flt
case response.ExecutionEventID: case response.ExecutionEventID:
if p.Type != request.ExecutionFilterT { flt := new(request.ExecutionFilter)
return nil, response.ErrInvalidParams err = jd.Decode(flt)
if err == nil && (flt.State == "HALT" || flt.State == "FAULT") {
p.Type = request.ExecutionFilterT
p.Value = *flt
} else if err == nil {
err = errors.New("invalid state")
} }
case response.NotaryRequestEventID:
if p.Type != request.TxFilterT {
return nil, response.ErrInvalidParams
} }
if err != nil {
return nil, response.ErrInvalidParams
} }
filter = p.Value filter = p.Value
} }