forked from TrueCloudLab/neoneo-go
neorpc: adjust SignerWithWitness scopes parsing
Ensure that Scopes can be properly parsed not only from the string representation, but also from a single byte. transaction.Signer is not affected (checked against the C# implementation), only RPC-related signer scopes are allowed to be unmarshalled from byte. Close #3059. Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
This commit is contained in:
parent
e30e262e66
commit
8db997c58a
4 changed files with 185 additions and 25 deletions
|
@ -3,6 +3,7 @@ package transaction
|
||||||
//go:generate stringer -type=WitnessScope -linecomment -output=witness_scope_string.go
|
//go:generate stringer -type=WitnessScope -linecomment -output=witness_scope_string.go
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
@ -29,6 +30,19 @@ const (
|
||||||
Global WitnessScope = 0x80
|
Global WitnessScope = 0x80
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ScopesFromByte converts byte to a set of WitnessScopes and performs validity
|
||||||
|
// check.
|
||||||
|
func ScopesFromByte(b byte) (WitnessScope, error) {
|
||||||
|
var res = WitnessScope(b)
|
||||||
|
if (res&Global != 0) && (res&(None|CalledByEntry|CustomContracts|CustomGroups|Rules) != 0) {
|
||||||
|
return 0, errors.New("Global scope can not be combined with other scopes")
|
||||||
|
}
|
||||||
|
if res&^(None|CalledByEntry|CustomContracts|CustomGroups|Rules|Global) != 0 {
|
||||||
|
return 0, fmt.Errorf("invalid scope %d", res)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ScopesFromString converts string of comma-separated scopes to a set of scopes
|
// ScopesFromString converts string of comma-separated scopes to a set of scopes
|
||||||
// (case-sensitive). String can combine several scopes, e.g. be any of: 'Global',
|
// (case-sensitive). String can combine several scopes, e.g. be any of: 'Global',
|
||||||
// 'CalledByEntry,CustomGroups' etc. In case of an empty string an error will be
|
// 'CalledByEntry,CustomGroups' etc. In case of an empty string an error will be
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package transaction
|
package transaction
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -51,3 +52,64 @@ func TestScopesFromString(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, CalledByEntry|CustomGroups|CustomContracts, s)
|
require.Equal(t, CalledByEntry|CustomGroups|CustomContracts, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestScopesFromByte(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
in byte
|
||||||
|
expected WitnessScope
|
||||||
|
shouldFail bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
in: 0,
|
||||||
|
expected: None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: 1,
|
||||||
|
expected: CalledByEntry,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: 16,
|
||||||
|
expected: CustomContracts,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: 32,
|
||||||
|
expected: CustomGroups,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: 64,
|
||||||
|
expected: Rules,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: 128,
|
||||||
|
expected: Global,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: 17,
|
||||||
|
expected: CalledByEntry | CustomContracts,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: 48,
|
||||||
|
expected: CustomContracts | CustomGroups,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: 128 + 1, // Global can't be combined with others.
|
||||||
|
shouldFail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: 2, // No such scope.
|
||||||
|
shouldFail: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(strconv.Itoa(int(tc.in)), func(t *testing.T) {
|
||||||
|
actual, err := ScopesFromByte(tc.in)
|
||||||
|
if tc.shouldFail {
|
||||||
|
require.Error(t, err, tc.in)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err, tc.in)
|
||||||
|
require.Equal(t, tc.expected, actual, tc.in)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -82,7 +82,7 @@ type (
|
||||||
// DisallowUnknownFields JSON marshaller setting.
|
// DisallowUnknownFields JSON marshaller setting.
|
||||||
type signerWithWitnessAux struct {
|
type signerWithWitnessAux struct {
|
||||||
Account string `json:"account"`
|
Account string `json:"account"`
|
||||||
Scopes transaction.WitnessScope `json:"scopes"`
|
Scopes json.RawMessage `json:"scopes"`
|
||||||
AllowedContracts []util.Uint160 `json:"allowedcontracts,omitempty"`
|
AllowedContracts []util.Uint160 `json:"allowedcontracts,omitempty"`
|
||||||
AllowedGroups []*keys.PublicKey `json:"allowedgroups,omitempty"`
|
AllowedGroups []*keys.PublicKey `json:"allowedgroups,omitempty"`
|
||||||
Rules []transaction.WitnessRule `json:"rules,omitempty"`
|
Rules []transaction.WitnessRule `json:"rules,omitempty"`
|
||||||
|
@ -92,9 +92,13 @@ type signerWithWitnessAux struct {
|
||||||
|
|
||||||
// MarshalJSON implements the json.Marshaler interface.
|
// MarshalJSON implements the json.Marshaler interface.
|
||||||
func (s *SignerWithWitness) MarshalJSON() ([]byte, error) {
|
func (s *SignerWithWitness) MarshalJSON() ([]byte, error) {
|
||||||
|
sc, err := s.Scopes.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal scopes: %w", err)
|
||||||
|
}
|
||||||
signer := &signerWithWitnessAux{
|
signer := &signerWithWitnessAux{
|
||||||
Account: s.Account.StringLE(),
|
Account: s.Account.StringLE(),
|
||||||
Scopes: s.Scopes,
|
Scopes: sc,
|
||||||
AllowedContracts: s.AllowedContracts,
|
AllowedContracts: s.AllowedContracts,
|
||||||
AllowedGroups: s.AllowedGroups,
|
AllowedGroups: s.AllowedGroups,
|
||||||
Rules: s.Rules,
|
Rules: s.Rules,
|
||||||
|
@ -118,9 +122,31 @@ func (s *SignerWithWitness) UnmarshalJSON(data []byte) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("not a signer: %w", err)
|
return fmt.Errorf("not a signer: %w", err)
|
||||||
}
|
}
|
||||||
|
var (
|
||||||
|
jStr string
|
||||||
|
jByte byte
|
||||||
|
scopes transaction.WitnessScope
|
||||||
|
)
|
||||||
|
if len(aux.Scopes) != 0 {
|
||||||
|
if err := json.Unmarshal(aux.Scopes, &jStr); err == nil {
|
||||||
|
scopes, err = transaction.ScopesFromString(jStr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to retrieve scopes from string: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := json.Unmarshal(aux.Scopes, &jByte)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal scopes from byte: %w", err)
|
||||||
|
}
|
||||||
|
scopes, err = transaction.ScopesFromByte(jByte)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to retrieve scopes from byte: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
s.Signer = transaction.Signer{
|
s.Signer = transaction.Signer{
|
||||||
Account: acc,
|
Account: acc,
|
||||||
Scopes: aux.Scopes,
|
Scopes: scopes,
|
||||||
AllowedContracts: aux.AllowedContracts,
|
AllowedContracts: aux.AllowedContracts,
|
||||||
AllowedGroups: aux.AllowedGroups,
|
AllowedGroups: aux.AllowedGroups,
|
||||||
Rules: aux.Rules,
|
Rules: aux.Rules,
|
||||||
|
|
|
@ -243,38 +243,96 @@ func TestGetWitness(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
raw string
|
raw string
|
||||||
expected neorpc.SignerWithWitness
|
expected neorpc.SignerWithWitness
|
||||||
|
shouldFail bool
|
||||||
}{
|
}{
|
||||||
{`{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569"}`, neorpc.SignerWithWitness{
|
{
|
||||||
Signer: transaction.Signer{
|
raw: `{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569"}`,
|
||||||
Account: accountHash,
|
expected: neorpc.SignerWithWitness{
|
||||||
Scopes: transaction.None,
|
Signer: transaction.Signer{
|
||||||
}},
|
Account: accountHash,
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{`{"account": "NYxb4fSZVKAz8YsgaPK2WkT3KcAE9b3Vag", "scopes": "Global"}`, neorpc.SignerWithWitness{
|
{
|
||||||
Signer: transaction.Signer{
|
raw: `{"account": "NYxb4fSZVKAz8YsgaPK2WkT3KcAE9b3Vag", "scopes": "Global"}`,
|
||||||
Account: addrHash,
|
expected: neorpc.SignerWithWitness{
|
||||||
Scopes: transaction.Global,
|
Signer: transaction.Signer{
|
||||||
}},
|
Account: addrHash,
|
||||||
|
Scopes: transaction.Global,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{`{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": "Global"}`, neorpc.SignerWithWitness{
|
{
|
||||||
Signer: transaction.Signer{
|
raw: `{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": "Global"}`,
|
||||||
Account: accountHash,
|
expected: neorpc.SignerWithWitness{
|
||||||
Scopes: transaction.Global,
|
Signer: transaction.Signer{
|
||||||
}},
|
Account: accountHash,
|
||||||
|
Scopes: transaction.Global,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
raw: `{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 128}`,
|
||||||
|
expected: neorpc.SignerWithWitness{
|
||||||
|
Signer: transaction.Signer{
|
||||||
|
Account: accountHash,
|
||||||
|
Scopes: transaction.Global,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
raw: `{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 0}`,
|
||||||
|
expected: neorpc.SignerWithWitness{
|
||||||
|
Signer: transaction.Signer{
|
||||||
|
Account: accountHash,
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
raw: `{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 1}`,
|
||||||
|
expected: neorpc.SignerWithWitness{
|
||||||
|
Signer: transaction.Signer{
|
||||||
|
Account: accountHash,
|
||||||
|
Scopes: transaction.CalledByEntry,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
raw: `{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 17}`,
|
||||||
|
expected: neorpc.SignerWithWitness{
|
||||||
|
Signer: transaction.Signer{
|
||||||
|
Account: accountHash,
|
||||||
|
Scopes: transaction.CalledByEntry | transaction.CustomContracts,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
raw: `{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 178}`,
|
||||||
|
shouldFail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
raw: `{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 2}`,
|
||||||
|
shouldFail: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
p := Param{RawMessage: json.RawMessage(tc.raw)}
|
p := Param{RawMessage: json.RawMessage(tc.raw)}
|
||||||
actual, err := p.GetSignerWithWitness()
|
actual, err := p.GetSignerWithWitness()
|
||||||
require.NoError(t, err)
|
if tc.shouldFail {
|
||||||
require.Equal(t, tc.expected, actual)
|
require.Error(t, err, tc.raw)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err, tc.raw)
|
||||||
|
require.Equal(t, tc.expected, actual)
|
||||||
|
|
||||||
actual, err = p.GetSignerWithWitness() // valid second invocation.
|
actual, err = p.GetSignerWithWitness() // valid second invocation.
|
||||||
require.NoError(t, err)
|
require.NoError(t, err, tc.raw)
|
||||||
require.Equal(t, tc.expected, actual)
|
require.Equal(t, tc.expected, actual)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue