forked from TrueCloudLab/neoneo-go
Merge pull request #1407 from nspcc-dev/core/oracleattr
Implement OracleResponse transaction attribute
This commit is contained in:
commit
a439941a71
8 changed files with 268 additions and 33 deletions
|
@ -1,7 +1,6 @@
|
||||||
package transaction
|
package transaction
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -11,29 +10,35 @@ import (
|
||||||
|
|
||||||
// Attribute represents a Transaction attribute.
|
// Attribute represents a Transaction attribute.
|
||||||
type Attribute struct {
|
type Attribute struct {
|
||||||
Type AttrType
|
Type AttrType
|
||||||
Data []byte
|
Value interface {
|
||||||
|
io.Serializable
|
||||||
|
// toJSONMap is used for embedded json struct marshalling.
|
||||||
|
// Anonymous interface fields are not considered anonymous by
|
||||||
|
// json lib and marshaling Value together with type makes code
|
||||||
|
// harder to follow.
|
||||||
|
toJSONMap(map[string]interface{})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// attrJSON is used for JSON I/O of Attribute.
|
// attrJSON is used for JSON I/O of Attribute.
|
||||||
type attrJSON struct {
|
type attrJSON struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Data string `json:"data"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeBinary implements Serializable interface.
|
// DecodeBinary implements Serializable interface.
|
||||||
func (attr *Attribute) DecodeBinary(br *io.BinReader) {
|
func (attr *Attribute) DecodeBinary(br *io.BinReader) {
|
||||||
attr.Type = AttrType(br.ReadB())
|
attr.Type = AttrType(br.ReadB())
|
||||||
|
|
||||||
var datasize uint64
|
|
||||||
switch attr.Type {
|
switch attr.Type {
|
||||||
case HighPriority:
|
case HighPriority:
|
||||||
|
case OracleResponseT:
|
||||||
|
attr.Value = new(OracleResponse)
|
||||||
|
attr.Value.DecodeBinary(br)
|
||||||
default:
|
default:
|
||||||
br.Err = fmt.Errorf("failed decoding TX attribute usage: 0x%2x", int(attr.Type))
|
br.Err = fmt.Errorf("failed decoding TX attribute usage: 0x%2x", int(attr.Type))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
attr.Data = make([]byte, datasize)
|
|
||||||
br.ReadBytes(attr.Data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncodeBinary implements Serializable interface.
|
// EncodeBinary implements Serializable interface.
|
||||||
|
@ -41,6 +46,8 @@ func (attr *Attribute) EncodeBinary(bw *io.BinWriter) {
|
||||||
bw.WriteB(byte(attr.Type))
|
bw.WriteB(byte(attr.Type))
|
||||||
switch attr.Type {
|
switch attr.Type {
|
||||||
case HighPriority:
|
case HighPriority:
|
||||||
|
case OracleResponseT:
|
||||||
|
attr.Value.EncodeBinary(bw)
|
||||||
default:
|
default:
|
||||||
bw.Err = fmt.Errorf("failed encoding TX attribute usage: 0x%2x", attr.Type)
|
bw.Err = fmt.Errorf("failed encoding TX attribute usage: 0x%2x", attr.Type)
|
||||||
}
|
}
|
||||||
|
@ -48,10 +55,11 @@ func (attr *Attribute) EncodeBinary(bw *io.BinWriter) {
|
||||||
|
|
||||||
// MarshalJSON implements the json Marshaller interface.
|
// MarshalJSON implements the json Marshaller interface.
|
||||||
func (attr *Attribute) MarshalJSON() ([]byte, error) {
|
func (attr *Attribute) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(attrJSON{
|
m := map[string]interface{}{"type": attr.Type.String()}
|
||||||
Type: attr.Type.String(),
|
if attr.Value != nil {
|
||||||
Data: base64.StdEncoding.EncodeToString(attr.Data),
|
attr.Value.toJSONMap(m)
|
||||||
})
|
}
|
||||||
|
return json.Marshal(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON implements the json.Unmarshaller interface.
|
// UnmarshalJSON implements the json.Unmarshaller interface.
|
||||||
|
@ -61,17 +69,18 @@ func (attr *Attribute) UnmarshalJSON(data []byte) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
binData, err := base64.StdEncoding.DecodeString(aj.Data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
switch aj.Type {
|
switch aj.Type {
|
||||||
case "HighPriority":
|
case "HighPriority":
|
||||||
attr.Type = HighPriority
|
attr.Type = HighPriority
|
||||||
|
case "OracleResponse":
|
||||||
|
attr.Type = OracleResponseT
|
||||||
|
// Note: because `type` field will not be present in any attribute
|
||||||
|
// value, we can unmarshal the same data. The overhead is minimal.
|
||||||
|
attr.Value = new(OracleResponse)
|
||||||
|
return json.Unmarshal(data, attr.Value)
|
||||||
default:
|
default:
|
||||||
return errors.New("wrong Type")
|
return errors.New("wrong Type")
|
||||||
|
|
||||||
}
|
}
|
||||||
attr.Data = binData
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
66
pkg/core/transaction/attribute_test.go
Normal file
66
pkg/core/transaction/attribute_test.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package transaction
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/internal/testserdes"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAttribute_EncodeBinary(t *testing.T) {
|
||||||
|
t.Run("HighPriority", func(t *testing.T) {
|
||||||
|
attr := &Attribute{
|
||||||
|
Type: HighPriority,
|
||||||
|
}
|
||||||
|
testserdes.EncodeDecodeBinary(t, attr, new(Attribute))
|
||||||
|
})
|
||||||
|
t.Run("OracleResponse", func(t *testing.T) {
|
||||||
|
attr := &Attribute{
|
||||||
|
Type: OracleResponseT,
|
||||||
|
Value: &OracleResponse{
|
||||||
|
ID: 0x1122334455,
|
||||||
|
Code: Success,
|
||||||
|
Result: []byte{1, 2, 3},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
testserdes.EncodeDecodeBinary(t, attr, new(Attribute))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAttribute_MarshalJSON(t *testing.T) {
|
||||||
|
t.Run("HighPriority", func(t *testing.T) {
|
||||||
|
attr := &Attribute{Type: HighPriority}
|
||||||
|
data, err := json.Marshal(attr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t, `{"type":"HighPriority"}`, string(data))
|
||||||
|
|
||||||
|
actual := new(Attribute)
|
||||||
|
require.NoError(t, json.Unmarshal(data, actual))
|
||||||
|
require.Equal(t, attr, actual)
|
||||||
|
})
|
||||||
|
t.Run("OracleResponse", func(t *testing.T) {
|
||||||
|
res := []byte{1, 2, 3}
|
||||||
|
attr := &Attribute{
|
||||||
|
Type: OracleResponseT,
|
||||||
|
Value: &OracleResponse{
|
||||||
|
ID: 123,
|
||||||
|
Code: Success,
|
||||||
|
Result: res,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(attr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t, `{
|
||||||
|
"type":"OracleResponse",
|
||||||
|
"id": 123,
|
||||||
|
"code": 0,
|
||||||
|
"result": "`+base64.StdEncoding.EncodeToString(res)+`"}`, string(data))
|
||||||
|
|
||||||
|
actual := new(Attribute)
|
||||||
|
require.NoError(t, json.Unmarshal(data, actual))
|
||||||
|
require.Equal(t, attr, actual)
|
||||||
|
testserdes.EncodeDecodeBinary(t, attr, new(Attribute))
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,11 +1,16 @@
|
||||||
package transaction
|
package transaction
|
||||||
|
|
||||||
//go:generate stringer -type=AttrType
|
//go:generate stringer -type=AttrType -linecomment
|
||||||
|
|
||||||
// AttrType represents the purpose of the attribute.
|
// AttrType represents the purpose of the attribute.
|
||||||
type AttrType uint8
|
type AttrType uint8
|
||||||
|
|
||||||
// List of valid attribute types.
|
// List of valid attribute types.
|
||||||
const (
|
const (
|
||||||
HighPriority AttrType = 1
|
HighPriority AttrType = 1
|
||||||
|
OracleResponseT AttrType = 0x11 // OracleResponse
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (a AttrType) allowMultiple() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Code generated by "stringer -type AttrType"; DO NOT EDIT.
|
// Code generated by "stringer -type=AttrType -linecomment"; DO NOT EDIT.
|
||||||
|
|
||||||
package transaction
|
package transaction
|
||||||
|
|
||||||
|
@ -9,16 +9,21 @@ func _() {
|
||||||
// Re-run the stringer command to generate them again.
|
// Re-run the stringer command to generate them again.
|
||||||
var x [1]struct{}
|
var x [1]struct{}
|
||||||
_ = x[HighPriority-1]
|
_ = x[HighPriority-1]
|
||||||
|
_ = x[OracleResponseT-17]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _AttrType_name = "HighPriority"
|
const (
|
||||||
|
_AttrType_name_0 = "HighPriority"
|
||||||
var _AttrType_index = [...]uint8{0, 12}
|
_AttrType_name_1 = "OracleResponse"
|
||||||
|
)
|
||||||
|
|
||||||
func (i AttrType) String() string {
|
func (i AttrType) String() string {
|
||||||
i -= 1
|
switch {
|
||||||
if i >= AttrType(len(_AttrType_index)-1) {
|
case i == 1:
|
||||||
return "AttrType(" + strconv.FormatInt(int64(i+1), 10) + ")"
|
return _AttrType_name_0
|
||||||
|
case i == 17:
|
||||||
|
return _AttrType_name_1
|
||||||
|
default:
|
||||||
|
return "AttrType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||||
}
|
}
|
||||||
return _AttrType_name[_AttrType_index[i]:_AttrType_index[i+1]]
|
|
||||||
}
|
}
|
||||||
|
|
66
pkg/core/transaction/oracle.go
Normal file
66
pkg/core/transaction/oracle.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package transaction
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OracleResponseCode represents result code of oracle response.
|
||||||
|
type OracleResponseCode byte
|
||||||
|
|
||||||
|
// OracleResponse represents oracle response.
|
||||||
|
type OracleResponse struct {
|
||||||
|
ID uint64 `json:"id"`
|
||||||
|
Code OracleResponseCode `json:"code"`
|
||||||
|
Result []byte `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxResultSize = 1024
|
||||||
|
|
||||||
|
// Enumeration of possible oracle response types.
|
||||||
|
const (
|
||||||
|
Success OracleResponseCode = 0x00
|
||||||
|
NotFound OracleResponseCode = 0x10
|
||||||
|
Timeout OracleResponseCode = 0x12
|
||||||
|
Forbidden OracleResponseCode = 0x14
|
||||||
|
Error OracleResponseCode = 0xff
|
||||||
|
)
|
||||||
|
|
||||||
|
// Various validation errors.
|
||||||
|
var (
|
||||||
|
ErrInvalidResponseCode = errors.New("invalid oracle response code")
|
||||||
|
ErrInvalidResult = errors.New("oracle response != success, but result is not empty")
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsValid checks if c is valid response code.
|
||||||
|
func (c OracleResponseCode) IsValid() bool {
|
||||||
|
return c == Success || c == NotFound || c == Timeout || c == Forbidden || c == Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeBinary implements io.Serializable interface.
|
||||||
|
func (r *OracleResponse) DecodeBinary(br *io.BinReader) {
|
||||||
|
r.ID = br.ReadU64LE()
|
||||||
|
r.Code = OracleResponseCode(br.ReadB())
|
||||||
|
if !r.Code.IsValid() {
|
||||||
|
br.Err = ErrInvalidResponseCode
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Result = br.ReadVarBytes(maxResultSize)
|
||||||
|
if r.Code != Success && len(r.Result) > 0 {
|
||||||
|
br.Err = ErrInvalidResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeBinary implements io.Serializable interface.
|
||||||
|
func (r *OracleResponse) EncodeBinary(w *io.BinWriter) {
|
||||||
|
w.WriteU64LE(r.ID)
|
||||||
|
w.WriteB(byte(r.Code))
|
||||||
|
w.WriteVarBytes(r.Result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OracleResponse) toJSONMap(m map[string]interface{}) {
|
||||||
|
m["id"] = r.ID
|
||||||
|
m["code"] = r.Code
|
||||||
|
m["result"] = r.Result
|
||||||
|
}
|
76
pkg/core/transaction/oracle_test.go
Normal file
76
pkg/core/transaction/oracle_test.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package transaction
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/internal/testserdes"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOracleResponse_EncodeBinary(t *testing.T) {
|
||||||
|
t.Run("Success", func(t *testing.T) {
|
||||||
|
r := &OracleResponse{
|
||||||
|
ID: rand.Uint64(),
|
||||||
|
Code: Success,
|
||||||
|
Result: []byte{1, 2, 3, 4, 5},
|
||||||
|
}
|
||||||
|
testserdes.EncodeDecodeBinary(t, r, new(OracleResponse))
|
||||||
|
})
|
||||||
|
t.Run("ErrorCodes", func(t *testing.T) {
|
||||||
|
codes := []OracleResponseCode{NotFound, Timeout, Forbidden, Error}
|
||||||
|
for _, c := range codes {
|
||||||
|
r := &OracleResponse{
|
||||||
|
ID: rand.Uint64(),
|
||||||
|
Code: c,
|
||||||
|
Result: []byte{},
|
||||||
|
}
|
||||||
|
testserdes.EncodeDecodeBinary(t, r, new(OracleResponse))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Error", func(t *testing.T) {
|
||||||
|
t.Run("InvalidCode", func(t *testing.T) {
|
||||||
|
r := &OracleResponse{
|
||||||
|
ID: rand.Uint64(),
|
||||||
|
Code: 0x42,
|
||||||
|
Result: []byte{},
|
||||||
|
}
|
||||||
|
bs, err := testserdes.EncodeBinary(r)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = testserdes.DecodeBinary(bs, new(OracleResponse))
|
||||||
|
require.True(t, errors.Is(err, ErrInvalidResponseCode), "got: %v", err)
|
||||||
|
})
|
||||||
|
t.Run("InvalidResult", func(t *testing.T) {
|
||||||
|
r := &OracleResponse{
|
||||||
|
ID: rand.Uint64(),
|
||||||
|
Code: Error,
|
||||||
|
Result: []byte{1},
|
||||||
|
}
|
||||||
|
bs, err := testserdes.EncodeBinary(r)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = testserdes.DecodeBinary(bs, new(OracleResponse))
|
||||||
|
require.True(t, errors.Is(err, ErrInvalidResult), "got: %v", err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOracleResponse_toJSONMap(t *testing.T) {
|
||||||
|
r := &OracleResponse{
|
||||||
|
ID: rand.Uint64(),
|
||||||
|
Code: Success,
|
||||||
|
Result: []byte{1},
|
||||||
|
}
|
||||||
|
|
||||||
|
b1, err := json.Marshal(r)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
m := map[string]interface{}{}
|
||||||
|
r.toJSONMap(m)
|
||||||
|
b2, err := json.Marshal(m)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t, string(b1), string(b2))
|
||||||
|
}
|
|
@ -381,14 +381,14 @@ func (t *Transaction) isValid() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hasHighPrio := false
|
attrs := map[AttrType]bool{}
|
||||||
for i := range t.Attributes {
|
for i := range t.Attributes {
|
||||||
switch t.Attributes[i].Type {
|
typ := t.Attributes[i].Type
|
||||||
case HighPriority:
|
if !typ.allowMultiple() {
|
||||||
if hasHighPrio {
|
if attrs[typ] {
|
||||||
return fmt.Errorf("%w: multiple high priority attributes", ErrInvalidAttribute)
|
return fmt.Errorf("%w: multiple '%s' attributes", ErrInvalidAttribute, typ.String())
|
||||||
}
|
}
|
||||||
hasHighPrio = true
|
attrs[typ] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(t.Script) == 0 {
|
if len(t.Script) == 0 {
|
||||||
|
|
|
@ -115,7 +115,7 @@ func TestMarshalUnmarshalJSONInvocationTX(t *testing.T) {
|
||||||
Version: 0,
|
Version: 0,
|
||||||
Signers: []Signer{{Account: util.Uint160{1, 2, 3}}},
|
Signers: []Signer{{Account: util.Uint160{1, 2, 3}}},
|
||||||
Script: []byte{1, 2, 3, 4},
|
Script: []byte{1, 2, 3, 4},
|
||||||
Attributes: []Attribute{{Type: HighPriority, Data: []byte{}}},
|
Attributes: []Attribute{{Type: HighPriority}},
|
||||||
Scripts: []Witness{},
|
Scripts: []Witness{},
|
||||||
Trimmed: false,
|
Trimmed: false,
|
||||||
}
|
}
|
||||||
|
@ -206,6 +206,14 @@ func TestTransaction_isValid(t *testing.T) {
|
||||||
}
|
}
|
||||||
require.True(t, errors.Is(tx.isValid(), ErrInvalidAttribute))
|
require.True(t, errors.Is(tx.isValid(), ErrInvalidAttribute))
|
||||||
})
|
})
|
||||||
|
t.Run("MultipleOracle", func(t *testing.T) {
|
||||||
|
tx := newTx()
|
||||||
|
tx.Attributes = []Attribute{
|
||||||
|
{Type: OracleResponseT},
|
||||||
|
{Type: OracleResponseT},
|
||||||
|
}
|
||||||
|
require.True(t, errors.Is(tx.isValid(), ErrInvalidAttribute))
|
||||||
|
})
|
||||||
t.Run("NoScript", func(t *testing.T) {
|
t.Run("NoScript", func(t *testing.T) {
|
||||||
tx := newTx()
|
tx := newTx()
|
||||||
tx.Script = []byte{}
|
tx.Script = []byte{}
|
||||||
|
|
Loading…
Reference in a new issue