Merge pull request #1407 from nspcc-dev/core/oracleattr

Implement OracleResponse transaction attribute
This commit is contained in:
Roman Khimov 2020-09-21 12:55:10 +03:00 committed by GitHub
commit a439941a71
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 268 additions and 33 deletions

View file

@ -1,7 +1,6 @@
package transaction
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@ -11,29 +10,35 @@ import (
// Attribute represents a Transaction attribute.
type Attribute struct {
Type AttrType
Data []byte
Type AttrType
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.
type attrJSON struct {
Type string `json:"type"`
Data string `json:"data"`
}
// DecodeBinary implements Serializable interface.
func (attr *Attribute) DecodeBinary(br *io.BinReader) {
attr.Type = AttrType(br.ReadB())
var datasize uint64
switch attr.Type {
case HighPriority:
case OracleResponseT:
attr.Value = new(OracleResponse)
attr.Value.DecodeBinary(br)
default:
br.Err = fmt.Errorf("failed decoding TX attribute usage: 0x%2x", int(attr.Type))
return
}
attr.Data = make([]byte, datasize)
br.ReadBytes(attr.Data)
}
// EncodeBinary implements Serializable interface.
@ -41,6 +46,8 @@ func (attr *Attribute) EncodeBinary(bw *io.BinWriter) {
bw.WriteB(byte(attr.Type))
switch attr.Type {
case HighPriority:
case OracleResponseT:
attr.Value.EncodeBinary(bw)
default:
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.
func (attr *Attribute) MarshalJSON() ([]byte, error) {
return json.Marshal(attrJSON{
Type: attr.Type.String(),
Data: base64.StdEncoding.EncodeToString(attr.Data),
})
m := map[string]interface{}{"type": attr.Type.String()}
if attr.Value != nil {
attr.Value.toJSONMap(m)
}
return json.Marshal(m)
}
// UnmarshalJSON implements the json.Unmarshaller interface.
@ -61,17 +69,18 @@ func (attr *Attribute) UnmarshalJSON(data []byte) error {
if err != nil {
return err
}
binData, err := base64.StdEncoding.DecodeString(aj.Data)
if err != nil {
return err
}
switch aj.Type {
case "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:
return errors.New("wrong Type")
}
attr.Data = binData
return nil
}

View 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))
})
}

View file

@ -1,11 +1,16 @@
package transaction
//go:generate stringer -type=AttrType
//go:generate stringer -type=AttrType -linecomment
// AttrType represents the purpose of the attribute.
type AttrType uint8
// List of valid attribute types.
const (
HighPriority AttrType = 1
HighPriority AttrType = 1
OracleResponseT AttrType = 0x11 // OracleResponse
)
func (a AttrType) allowMultiple() bool {
return false
}

View file

@ -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
@ -9,16 +9,21 @@ func _() {
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[HighPriority-1]
_ = x[OracleResponseT-17]
}
const _AttrType_name = "HighPriority"
var _AttrType_index = [...]uint8{0, 12}
const (
_AttrType_name_0 = "HighPriority"
_AttrType_name_1 = "OracleResponse"
)
func (i AttrType) String() string {
i -= 1
if i >= AttrType(len(_AttrType_index)-1) {
return "AttrType(" + strconv.FormatInt(int64(i+1), 10) + ")"
switch {
case i == 1:
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]]
}

View 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
}

View 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))
}

View file

@ -381,14 +381,14 @@ func (t *Transaction) isValid() error {
}
}
}
hasHighPrio := false
attrs := map[AttrType]bool{}
for i := range t.Attributes {
switch t.Attributes[i].Type {
case HighPriority:
if hasHighPrio {
return fmt.Errorf("%w: multiple high priority attributes", ErrInvalidAttribute)
typ := t.Attributes[i].Type
if !typ.allowMultiple() {
if attrs[typ] {
return fmt.Errorf("%w: multiple '%s' attributes", ErrInvalidAttribute, typ.String())
}
hasHighPrio = true
attrs[typ] = true
}
}
if len(t.Script) == 0 {

View file

@ -115,7 +115,7 @@ func TestMarshalUnmarshalJSONInvocationTX(t *testing.T) {
Version: 0,
Signers: []Signer{{Account: util.Uint160{1, 2, 3}}},
Script: []byte{1, 2, 3, 4},
Attributes: []Attribute{{Type: HighPriority, Data: []byte{}}},
Attributes: []Attribute{{Type: HighPriority}},
Scripts: []Witness{},
Trimmed: false,
}
@ -206,6 +206,14 @@ func TestTransaction_isValid(t *testing.T) {
}
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) {
tx := newTx()
tx.Script = []byte{}