vm: allow to emit convertible

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
This commit is contained in:
Anna Shaleva 2023-05-15 18:04:32 +03:00
parent 8e085d3ca3
commit 15138b2004
10 changed files with 574 additions and 23 deletions

View file

@ -12,6 +12,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/neotest"
"github.com/nspcc-dev/neo-go/pkg/neotest/chain"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/notary"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
@ -75,19 +76,19 @@ func TestNotary_Pipeline(t *testing.T) {
notaryCommitteeInvoker.Invoke(t, false, "lockDepositUntil", multisigHash, int64(depositLock+1))
// `onPayment`: bad token
neoCommitteeInvoker.InvokeFail(t, "only GAS can be accepted for deposit", "transfer", multisigHash, notaryHash, int64(1), []any{nil, int64(depositLock)})
neoCommitteeInvoker.InvokeFail(t, "only GAS can be accepted for deposit", "transfer", multisigHash, notaryHash, int64(1), &notary.OnNEP17PaymentData{Till: uint32(depositLock)})
// `onPayment`: insufficient first deposit
gasCommitteeInvoker.InvokeFail(t, "first deposit can not be less then", "transfer", multisigHash, notaryHash, int64(2*feePerKey-1), []any{nil, int64(depositLock)})
gasCommitteeInvoker.InvokeFail(t, "first deposit can not be less then", "transfer", multisigHash, notaryHash, int64(2*feePerKey-1), &notary.OnNEP17PaymentData{Till: uint32(depositLock)})
// `onPayment`: invalid `data` (missing `till` parameter)
gasCommitteeInvoker.InvokeFail(t, "`data` parameter should be an array of 2 elements", "transfer", multisigHash, notaryHash, 2*feePerKey, []any{nil})
// `onPayment`: invalid `data` (outdated `till` parameter)
gasCommitteeInvoker.InvokeFail(t, "`till` shouldn't be less then the chain's height", "transfer", multisigHash, notaryHash, 2*feePerKey, []any{nil, int64(0)})
gasCommitteeInvoker.InvokeFail(t, "`till` shouldn't be less then the chain's height", "transfer", multisigHash, notaryHash, 2*feePerKey, &notary.OnNEP17PaymentData{})
// `onPayment`: good
gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, 2*feePerKey, []any{nil, int64(depositLock)})
gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, 2*feePerKey, &notary.OnNEP17PaymentData{Till: uint32(depositLock)})
checkBalanceOf(t, notaryHash, 2*feePerKey)
// `expirationOf`: check `till` was set
@ -97,7 +98,7 @@ func TestNotary_Pipeline(t *testing.T) {
notaryCommitteeInvoker.Invoke(t, 2*feePerKey, "balanceOf", multisigHash)
// `onPayment`: good second deposit and explicit `to` paramenter
gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, feePerKey, []any{multisigHash, int64(depositLock + 1)})
gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, feePerKey, &notary.OnNEP17PaymentData{Account: &multisigHash, Till: uint32(depositLock + 1)})
checkBalanceOf(t, notaryHash, 3*feePerKey)
// `balanceOf`: check deposited amount for the multisig account
@ -107,17 +108,17 @@ func TestNotary_Pipeline(t *testing.T) {
notaryCommitteeInvoker.Invoke(t, depositLock+1, "expirationOf", multisigHash)
// `onPayment`: empty payment, should fail because `till` less then the previous one
gasCommitteeInvoker.InvokeFail(t, "`till` shouldn't be less then the previous value", "transfer", multisigHash, notaryHash, int64(0), []any{multisigHash, int64(depositLock)})
gasCommitteeInvoker.InvokeFail(t, "`till` shouldn't be less then the previous value", "transfer", multisigHash, notaryHash, int64(0), &notary.OnNEP17PaymentData{Account: &multisigHash, Till: uint32(depositLock)})
checkBalanceOf(t, notaryHash, 3*feePerKey)
notaryCommitteeInvoker.Invoke(t, depositLock+1, "expirationOf", multisigHash)
// `onPayment`: empty payment, should fail because `till` less then the chain height
gasCommitteeInvoker.InvokeFail(t, "`till` shouldn't be less then the chain's height", "transfer", multisigHash, notaryHash, int64(0), []any{multisigHash, int64(1)})
gasCommitteeInvoker.InvokeFail(t, "`till` shouldn't be less then the chain's height", "transfer", multisigHash, notaryHash, int64(0), &notary.OnNEP17PaymentData{Account: &multisigHash, Till: uint32(1)})
checkBalanceOf(t, notaryHash, 3*feePerKey)
notaryCommitteeInvoker.Invoke(t, depositLock+1, "expirationOf", multisigHash)
// `onPayment`: empty payment, should successfully update `till`
gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, int64(0), []any{multisigHash, int64(depositLock + 2)})
gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, int64(0), &notary.OnNEP17PaymentData{Account: &multisigHash, Till: uint32(depositLock + 2)})
checkBalanceOf(t, notaryHash, 3*feePerKey)
notaryCommitteeInvoker.Invoke(t, depositLock+2, "expirationOf", multisigHash)
@ -159,12 +160,12 @@ func TestNotary_Pipeline(t *testing.T) {
notaryCommitteeInvoker.Invoke(t, false, "withdraw", multisigHash, accHash)
// `onPayment`: good first deposit to other account, should set default `till` even if other `till` value is provided
gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, 2*feePerKey, []any{accHash, int64(math.MaxUint32 - 1)})
gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, 2*feePerKey, &notary.OnNEP17PaymentData{Account: &accHash, Till: uint32(math.MaxUint32 - 1)})
checkBalanceOf(t, notaryHash, 2*feePerKey)
notaryCommitteeInvoker.Invoke(t, 5760+e.Chain.BlockHeight()-1, "expirationOf", accHash)
// `onPayment`: good second deposit to other account, shouldn't update `till` even if other `till` value is provided
gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, feePerKey, []any{accHash, int64(math.MaxUint32 - 1)})
gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, feePerKey, &notary.OnNEP17PaymentData{Account: &accHash, Till: uint32(math.MaxUint32 - 1)})
checkBalanceOf(t, notaryHash, 3*feePerKey)
notaryCommitteeInvoker.Invoke(t, 5760+e.Chain.BlockHeight()-3, "expirationOf", accHash)
}
@ -201,7 +202,7 @@ func TestNotary_NotaryNodesReward(t *testing.T) {
if !spendFullDeposit {
depositAmount += 1_0000
}
gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, depositAmount, []any{multisigHash, e.Chain.BlockHeight() + 1})
gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, depositAmount, &notary.OnNEP17PaymentData{Account: &multisigHash, Till: e.Chain.BlockHeight() + 1})
// send transaction with Notary contract as a sender
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 1_000_000)

View file

@ -217,6 +217,22 @@ func TestReaderTokensOf(t *testing.T) {
}
}
type tData struct {
someInt int
someString string
}
func (d *tData) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.Make(d.someInt),
stackitem.Make(d.someString),
}), nil
}
func (d *tData) FromStackItem(si stackitem.Item) error {
panic("TODO")
}
func TestTokenTransfer(t *testing.T) {
ta := new(testAct)
tok := NewBase(ta, util.Uint160{1, 2, 3})
@ -233,7 +249,18 @@ func TestTokenTransfer(t *testing.T) {
require.Equal(t, ta.txh, h)
require.Equal(t, ta.vub, vub)
_, _, err = tok.Transfer(util.Uint160{3, 2, 1}, []byte{3, 2, 1}, stackitem.NewMap())
ta.err = nil
ta.txh = util.Uint256{1, 2, 3}
ta.vub = 42
h, vub, err = tok.Transfer(util.Uint160{3, 2, 1}, []byte{3, 2, 1}, &tData{
someInt: 5,
someString: "ur",
})
require.NoError(t, err)
require.Equal(t, ta.txh, h)
require.Equal(t, ta.vub, vub)
_, _, err = tok.Transfer(util.Uint160{3, 2, 1}, []byte{3, 2, 1}, stackitem.NewPointer(123, []byte{123}))
require.Error(t, err)
}
@ -255,7 +282,16 @@ func TestTokenTransferTransaction(t *testing.T) {
require.NoError(t, err)
require.Equal(t, ta.tx, tx)
_, err = fun(util.Uint160{3, 2, 1}, []byte{3, 2, 1}, stackitem.NewMap())
ta.err = nil
ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42}
tx, err = fun(util.Uint160{3, 2, 1}, []byte{3, 2, 1}, &tData{
someInt: 5,
someString: "ur",
})
require.NoError(t, err)
require.Equal(t, ta.tx, tx)
_, err = fun(util.Uint160{3, 2, 1}, []byte{3, 2, 1}, stackitem.NewInterop(nil))
require.Error(t, err)
}
}

View file

@ -190,7 +190,18 @@ func TestDivisibleTransfer(t *testing.T) {
require.Equal(t, ta.txh, h)
require.Equal(t, ta.vub, vub)
_, _, err = tok.TransferD(util.Uint160{1, 2, 3}, util.Uint160{3, 2, 1}, big.NewInt(10), []byte{3, 2, 1}, stackitem.NewMap())
ta.err = nil
ta.txh = util.Uint256{1, 2, 3}
ta.vub = 42
h, vub, err = tok.TransferD(util.Uint160{1, 2, 3}, util.Uint160{3, 2, 1}, big.NewInt(10), []byte{3, 2, 1}, &tData{
someInt: 5,
someString: "ur",
})
require.NoError(t, err)
require.Equal(t, ta.txh, h)
require.Equal(t, ta.vub, vub)
_, _, err = tok.TransferD(util.Uint160{1, 2, 3}, util.Uint160{3, 2, 1}, big.NewInt(10), []byte{3, 2, 1}, stackitem.NewInterop(nil))
require.Error(t, err)
}
@ -212,7 +223,16 @@ func TestDivisibleTransferTransaction(t *testing.T) {
require.NoError(t, err)
require.Equal(t, ta.tx, tx)
_, err = fun(util.Uint160{1, 2, 3}, util.Uint160{3, 2, 1}, big.NewInt(10), []byte{3, 2, 1}, stackitem.NewMap())
ta.err = nil
ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42}
tx, err = fun(util.Uint160{1, 2, 3}, util.Uint160{3, 2, 1}, big.NewInt(10), []byte{3, 2, 1}, &tData{
someInt: 5,
someString: "ur",
})
require.NoError(t, err)
require.Equal(t, ta.tx, tx)
_, err = fun(util.Uint160{1, 2, 3}, util.Uint160{3, 2, 1}, big.NewInt(10), []byte{3, 2, 1}, stackitem.NewInterop(nil))
require.Error(t, err)
}
}

View file

@ -62,6 +62,22 @@ func TestReaderBalanceOf(t *testing.T) {
require.Error(t, err)
}
type tData struct {
someInt int
someString string
}
func (d *tData) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.Make(d.someInt),
stackitem.Make(d.someString),
}), nil
}
func (d *tData) FromStackItem(si stackitem.Item) error {
panic("TODO")
}
func TestTokenTransfer(t *testing.T) {
ta := new(testAct)
tok := New(ta, util.Uint160{1, 2, 3})
@ -85,7 +101,18 @@ func TestTokenTransfer(t *testing.T) {
require.Equal(t, ta.txh, h)
require.Equal(t, ta.vub, vub)
_, _, err = fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), stackitem.NewMap())
ta.err = nil
ta.txh = util.Uint256{1, 2, 3}
ta.vub = 42
h, vub, err = fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), &tData{
someInt: 5,
someString: "ur",
})
require.NoError(t, err)
require.Equal(t, ta.txh, h)
require.Equal(t, ta.vub, vub)
_, _, err = fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), stackitem.NewInterop(nil))
require.Error(t, err)
})
}
@ -120,7 +147,16 @@ func TestTokenTransferTransaction(t *testing.T) {
require.NoError(t, err)
require.Equal(t, ta.tx, tx)
_, err = fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), stackitem.NewMap())
ta.err = nil
ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42}
tx, err = fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), &tData{
someInt: 5,
someString: "ur",
})
require.NoError(t, err)
require.Equal(t, ta.tx, tx)
_, err = fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), stackitem.NewInterop(nil))
require.Error(t, err)
})
}

View file

@ -8,6 +8,8 @@ creation of notary requests.
package notary
import (
"errors"
"fmt"
"math"
"math/big"
@ -18,6 +20,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
const (
@ -68,6 +71,10 @@ type OnNEP17PaymentData struct {
Till uint32
}
// OnNEP17PaymentData have to implement stackitem.Convertible interface to be
// compatible with emit package.
var _ = stackitem.Convertible(&OnNEP17PaymentData{})
// Hash stores the hash of the native Notary contract.
var Hash = state.CreateNativeContractHash(nativenames.Notary)
@ -234,3 +241,48 @@ func withdrawScript(from util.Uint160, to util.Uint160) []byte {
script, _ := smartcontract.CreateCallWithAssertScript(Hash, "withdraw", from.BytesBE(), to.BytesBE())
return script
}
// ToStackItem implements stackitem.Convertible interface.
func (d *OnNEP17PaymentData) ToStackItem() (stackitem.Item, error) {
return stackitem.NewArray([]stackitem.Item{
stackitem.Make(d.Account),
stackitem.Make(d.Till),
}), nil
}
// FromStackItem implements stackitem.Convertible interface.
func (d *OnNEP17PaymentData) FromStackItem(si stackitem.Item) error {
arr, ok := si.Value().([]stackitem.Item)
if !ok {
return fmt.Errorf("unexpected stackitem type: %s", si.Type())
}
if len(arr) != 2 {
return fmt.Errorf("unexpected number of fields: %d vs %d", len(arr), 2)
}
if arr[0] != stackitem.Item(stackitem.Null{}) {
accBytes, err := arr[0].TryBytes()
if err != nil {
return fmt.Errorf("failed to retrieve account bytes: %w", err)
}
acc, err := util.Uint160DecodeBytesBE(accBytes)
if err != nil {
return fmt.Errorf("failed to decode account bytes: %w", err)
}
d.Account = &acc
}
till, err := arr[1].TryInteger()
if err != nil {
return fmt.Errorf("failed to retrieve till: %w", err)
}
if !till.IsInt64() {
return errors.New("till is not an int64")
}
val := till.Int64()
if val > math.MaxUint32 {
return fmt.Errorf("till is larger than max uint32 value: %d", val)
}
d.Till = uint32(val)
return nil
}

View file

@ -2,9 +2,12 @@ package notary
import (
"errors"
"math"
"math/big"
"strings"
"testing"
"github.com/nspcc-dev/neo-go/internal/testserdes"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/util"
@ -197,3 +200,75 @@ func TestTxMakers(t *testing.T) {
})
}
}
func TestOnNEP17PaymentData_Convertible(t *testing.T) {
t.Run("non-empty owner", func(t *testing.T) {
d := &OnNEP17PaymentData{
Account: &util.Uint160{1, 2, 3},
Till: 123,
}
testserdes.ToFromStackItem(t, d, new(OnNEP17PaymentData))
})
t.Run("empty owner", func(t *testing.T) {
d := &OnNEP17PaymentData{
Account: nil,
Till: 123,
}
testserdes.ToFromStackItem(t, d, new(OnNEP17PaymentData))
})
}
func TestOnNEP17PaymentDataToStackItem(t *testing.T) {
testCases := map[string]struct {
data *OnNEP17PaymentData
expected stackitem.Item
}{
"non-empty owner": {
data: &OnNEP17PaymentData{
Account: &util.Uint160{1, 2, 3},
Till: 123,
},
expected: stackitem.NewArray([]stackitem.Item{
stackitem.Make(util.Uint160{1, 2, 3}),
stackitem.Make(123),
}),
},
"empty owner": {
data: &OnNEP17PaymentData{
Account: nil,
Till: 123,
},
expected: stackitem.NewArray([]stackitem.Item{
stackitem.Null{},
stackitem.Make(123),
}),
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
actual, err := tc.data.ToStackItem()
require.NoError(t, err)
require.Equal(t, tc.expected, actual)
})
}
}
func TestOnNEP17PaymentData_FromStackItem(t *testing.T) {
errCases := map[string]stackitem.Item{
"unexpected stackitem type": stackitem.NewBool(true),
"unexpected number of fields": stackitem.NewArray([]stackitem.Item{stackitem.NewBool(true)}),
"failed to retrieve account bytes": stackitem.NewArray([]stackitem.Item{stackitem.NewInterop(nil), stackitem.Make(1)}),
"failed to decode account bytes": stackitem.NewArray([]stackitem.Item{stackitem.Make([]byte{1}), stackitem.Make(1)}),
"failed to retrieve till": stackitem.NewArray([]stackitem.Item{stackitem.Make(util.Uint160{1}), stackitem.NewInterop(nil)}),
"till is not an int64": stackitem.NewArray([]stackitem.Item{stackitem.Make(util.Uint160{1}), stackitem.NewBigInteger(new(big.Int).Add(big.NewInt(math.MaxInt64), big.NewInt(1)))}),
"till is larger than max uint32 value": stackitem.NewArray([]stackitem.Item{stackitem.Make(util.Uint160{1}), stackitem.Make(math.MaxUint32 + 1)}),
}
for name, errCase := range errCases {
t.Run(name, func(t *testing.T) {
d := new(OnNEP17PaymentData)
err := d.FromStackItem(errCase)
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), name), name)
})
}
}

View file

@ -32,7 +32,7 @@ func ExampleActor() {
// Transfer some GAS to the Notary contract to be able to send notary requests
// from the first account.
gasSingle := gas.New(single)
txid, vub, _ := gasSingle.Transfer(single.Sender(), notary.Hash, big.NewInt(10_0000_0000), notary.OnNEP17PaymentData{Till: 10000000})
txid, vub, _ := gasSingle.Transfer(single.Sender(), notary.Hash, big.NewInt(10_0000_0000), &notary.OnNEP17PaymentData{Till: 10000000})
var depositOK bool
// Wait for transaction to be persisted, either it gets in and we get

View file

@ -41,9 +41,10 @@ func NewBuilder() *Builder {
// InvokeMethod is the most generic contract method invoker, the code it produces
// packs all of the arguments given into an array and calls some method of the
// contract. The correctness of this invocation (number and type of parameters) is
// out of scope of this method, as well as return value, if contract's method returns
// something this value just remains on the execution stack.
// contract. It accepts as parameters everything that emit.Array accepts. The
// correctness of this invocation (number and type of parameters) is out of scope
// of this method, as well as return value, if contract's method returns something
// this value just remains on the execution stack.
func (b *Builder) InvokeMethod(contract util.Uint160, method string, params ...any) {
emit.AppCall(b.bw.BinWriter, contract, method, callflag.All, params...)
}

View file

@ -98,7 +98,16 @@ func bigInt(w *io.BinWriter, n *big.Int, trySmall bool) {
w.WriteBytes(padRight(1<<padSize, buf))
}
// Array emits an array of elements to the given buffer.
// Array emits an array of elements to the given buffer. It accepts elements of the following types:
// - int8, int16, int32, int64, int
// - uint8, uint16, uint32
// - *big.Int
// - string, []byte
// - util.Uint160, *util.Uint160, util.Uint256, *util.Uint256
// - bool
// - stackitem.Convertible, stackitem.Item
// - nil
// - []any
func Array(w *io.BinWriter, es ...any) {
if len(es) == 0 {
Opcodes(w, opcode.NEWARRAY0)
@ -148,6 +157,10 @@ func Array(w *io.BinWriter, es ...any) {
Bytes(w, e)
case bool:
Bool(w, e)
case stackitem.Convertible:
Convertible(w, e)
case stackitem.Item:
StackItem(w, e)
default:
if es[i] != nil {
w.Err = fmt.Errorf("unsupported type: %T", e)
@ -160,6 +173,63 @@ func Array(w *io.BinWriter, es ...any) {
Opcodes(w, opcode.PACK)
}
// Convertible converts provided stackitem.Convertible to the stackitem.Item and
// emits the item to the given buffer.
func Convertible(w *io.BinWriter, c stackitem.Convertible) {
si, err := c.ToStackItem()
if err != nil {
w.Err = fmt.Errorf("failed to convert stackitem.Convertible to stackitem: %w", err)
return
}
StackItem(w, si)
}
// StackItem emits provided stackitem.Item to the given buffer.
func StackItem(w *io.BinWriter, si stackitem.Item) {
switch t := si.Type(); t {
case stackitem.AnyT:
if si.Value() == nil {
Opcodes(w, opcode.PUSHNULL)
} else {
w.Err = fmt.Errorf("only nil value supported for %s", t)
return
}
case stackitem.BooleanT:
Bool(w, si.Value().(bool))
case stackitem.IntegerT:
BigInt(w, si.Value().(*big.Int))
case stackitem.ByteArrayT, stackitem.BufferT:
Bytes(w, si.Value().([]byte))
case stackitem.ArrayT:
arr := si.Value().([]stackitem.Item)
arrAny := make([]any, len(arr))
for i := range arr {
arrAny[i] = arr[i]
}
Array(w, arrAny...)
case stackitem.StructT:
arr := si.Value().([]stackitem.Item)
for i := len(arr) - 1; i >= 0; i-- {
StackItem(w, arr[i])
}
Int(w, int64(len(arr)))
Opcodes(w, opcode.PACKSTRUCT)
case stackitem.MapT:
arr := si.Value().([]stackitem.MapElement)
for i := len(arr) - 1; i >= 0; i-- {
StackItem(w, arr[i].Value)
StackItem(w, arr[i].Key)
}
Int(w, int64(len(arr)))
Opcodes(w, opcode.PACKMAP)
default:
w.Err = fmt.Errorf("%s is unsuppoted", t)
return
}
}
// String emits a string to the given buffer.
func String(w *io.BinWriter, s string) {
Bytes(w, []byte(s))

View file

@ -5,6 +5,7 @@ import (
"errors"
"math"
"math/big"
"strings"
"testing"
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
@ -223,7 +224,32 @@ func TestEmitArray(t *testing.T) {
u256 := util.Uint256{1, 2, 3}
veryBig := new(big.Int).SetUint64(math.MaxUint64)
veryBig.Add(veryBig, big.NewInt(1))
Array(buf.BinWriter, p160, p256, &u160, &u256, u160, u256, big.NewInt(0), veryBig,
Array(buf.BinWriter,
stackitem.NewMapWithValue([]stackitem.MapElement{
{
Key: stackitem.Make(1),
Value: stackitem.Make("str1"),
},
{
Key: stackitem.Make(2),
Value: stackitem.Make("str2"),
},
}),
stackitem.NewStruct([]stackitem.Item{
stackitem.Make(4),
stackitem.Make("str"),
}),
&ConvertibleStruct{
SomeInt: 5,
SomeString: "str",
},
stackitem.Make(5),
stackitem.Make("str"),
stackitem.NewArray([]stackitem.Item{
stackitem.Make(true),
stackitem.Make("str"),
}),
p160, p256, &u160, &u256, u160, u256, big.NewInt(0), veryBig,
[]any{int64(1), int64(2)}, nil, int64(1), "str", false, true, []byte{0xCA, 0xFE})
require.NoError(t, buf.Err)
@ -259,6 +285,52 @@ func TestEmitArray(t *testing.T) {
assert.EqualValues(t, u160.BytesBE(), res[127:147])
assert.EqualValues(t, opcode.PUSHNULL, res[147])
assert.EqualValues(t, opcode.PUSHNULL, res[148])
// Array of two stackitems:
assert.EqualValues(t, opcode.PUSHDATA1, res[149])
assert.EqualValues(t, 3, res[150])
assert.EqualValues(t, []byte("str"), res[151:154])
assert.EqualValues(t, opcode.PUSHT, res[154])
assert.EqualValues(t, opcode.PUSH2, res[155])
assert.EqualValues(t, opcode.PACK, res[156])
// ByteString stackitem ("str"):
assert.EqualValues(t, opcode.PUSHDATA1, res[157])
assert.EqualValues(t, 3, res[158])
assert.EqualValues(t, []byte("str"), res[159:162])
// Integer stackitem (5):
assert.EqualValues(t, opcode.PUSH5, res[162])
// Convertible struct:
assert.EqualValues(t, opcode.PUSHDATA1, res[163])
assert.EqualValues(t, 3, res[164])
assert.EqualValues(t, []byte("str"), res[165:168])
assert.EqualValues(t, opcode.PUSH5, res[168])
assert.EqualValues(t, opcode.PUSH2, res[169])
assert.EqualValues(t, opcode.PACK, res[170])
// Struct stackitem (4, "str")
assert.EqualValues(t, opcode.PUSHDATA1, res[171])
assert.EqualValues(t, 3, res[172])
assert.EqualValues(t, []byte("str"), res[173:176])
assert.EqualValues(t, opcode.PUSH4, res[176])
assert.EqualValues(t, opcode.PUSH2, res[177])
assert.EqualValues(t, opcode.PACKSTRUCT, res[178])
// Map stackitem (1:"str1", 2:"str2")
assert.EqualValues(t, opcode.PUSHDATA1, res[179])
assert.EqualValues(t, 4, res[180])
assert.EqualValues(t, []byte("str2"), res[181:185])
assert.EqualValues(t, opcode.PUSH2, res[185])
assert.EqualValues(t, opcode.PUSHDATA1, res[186])
assert.EqualValues(t, 4, res[187])
assert.EqualValues(t, []byte("str1"), res[188:192])
assert.EqualValues(t, opcode.PUSH1, res[192])
assert.EqualValues(t, opcode.PUSH2, res[193])
assert.EqualValues(t, opcode.PACKMAP, res[194])
// Values packing:
assert.EqualValues(t, opcode.PUSHINT8, res[195])
assert.EqualValues(t, byte(21), res[196])
assert.EqualValues(t, opcode.PACK, res[197])
// Overall script length:
assert.EqualValues(t, 198, len(res))
})
t.Run("empty", func(t *testing.T) {
@ -374,3 +446,191 @@ func TestEmitCall(t *testing.T) {
label := binary.LittleEndian.Uint16(result[1:3])
assert.Equal(t, label, uint16(100))
}
func TestEmitStackitem(t *testing.T) {
t.Run("good", func(t *testing.T) {
buf := io.NewBufBinWriter()
itms := []stackitem.Item{
stackitem.Make(true),
stackitem.Make(false),
stackitem.Make(5),
stackitem.Make("str"),
stackitem.Make([]stackitem.Item{
stackitem.Make(true),
stackitem.Make([]stackitem.Item{
stackitem.Make(1),
stackitem.Make("str"),
}),
}),
stackitem.NewStruct([]stackitem.Item{
stackitem.Make(true),
stackitem.Make(7),
}),
stackitem.NewMapWithValue([]stackitem.MapElement{
{
Key: stackitem.Make(7),
Value: stackitem.Make("str1"),
},
{
Key: stackitem.Make(8),
Value: stackitem.Make("str2"),
},
}),
stackitem.Null{},
}
for _, si := range itms {
StackItem(buf.BinWriter, si)
}
require.NoError(t, buf.Err)
res := buf.Bytes()
// Single values:
assert.EqualValues(t, opcode.PUSHT, res[0])
assert.EqualValues(t, opcode.PUSHF, res[1])
assert.EqualValues(t, opcode.PUSH5, res[2])
assert.EqualValues(t, opcode.PUSHDATA1, res[3])
assert.EqualValues(t, 3, res[4])
assert.EqualValues(t, []byte("str"), res[5:8])
// Nested array:
assert.EqualValues(t, opcode.PUSHDATA1, res[8])
assert.EqualValues(t, 3, res[9])
assert.EqualValues(t, []byte("str"), res[10:13])
assert.EqualValues(t, opcode.PUSH1, res[13])
assert.EqualValues(t, opcode.PUSH2, res[14])
assert.EqualValues(t, opcode.PACK, res[15])
assert.EqualValues(t, opcode.PUSHT, res[16])
assert.EqualValues(t, opcode.PUSH2, res[17])
assert.EqualValues(t, opcode.PACK, res[18])
// Struct (true, 7):
assert.EqualValues(t, opcode.PUSH7, res[19])
assert.EqualValues(t, opcode.PUSHT, res[20])
assert.EqualValues(t, opcode.PUSH2, res[21])
assert.EqualValues(t, opcode.PACKSTRUCT, res[22])
// Map (7:"str1", 8:"str2"):
assert.EqualValues(t, opcode.PUSHDATA1, res[23])
assert.EqualValues(t, 4, res[24])
assert.EqualValues(t, []byte("str2"), res[25:29])
assert.EqualValues(t, opcode.PUSH8, res[29])
assert.EqualValues(t, opcode.PUSHDATA1, res[30])
assert.EqualValues(t, 4, res[31])
assert.EqualValues(t, []byte("str1"), res[32:36])
assert.EqualValues(t, opcode.PUSH7, res[36])
assert.EqualValues(t, opcode.PUSH2, res[37])
assert.EqualValues(t, opcode.PACKMAP, res[38])
// Null:
assert.EqualValues(t, opcode.PUSHNULL, res[39])
// Overall script length:
require.Equal(t, 40, len(res))
})
t.Run("unsupported", func(t *testing.T) {
itms := []stackitem.Item{
stackitem.NewInterop(nil),
stackitem.NewPointer(123, []byte{123}),
}
for _, si := range itms {
buf := io.NewBufBinWriter()
StackItem(buf.BinWriter, si)
require.Error(t, buf.Err)
}
})
t.Run("invalid any", func(t *testing.T) {
buf := io.NewBufBinWriter()
StackItem(buf.BinWriter, StrangeStackItem{})
actualErr := buf.Err
require.Error(t, actualErr)
require.True(t, strings.Contains(actualErr.Error(), "only nil value supported"), actualErr.Error())
})
}
type StrangeStackItem struct{}
var _ = stackitem.Item(StrangeStackItem{})
func (StrangeStackItem) Value() any {
return struct{}{}
}
func (StrangeStackItem) Type() stackitem.Type {
return stackitem.AnyT
}
func (StrangeStackItem) String() string {
panic("TODO")
}
func (StrangeStackItem) Dup() stackitem.Item {
panic("TODO")
}
func (StrangeStackItem) TryBool() (bool, error) {
panic("TODO")
}
func (StrangeStackItem) TryBytes() ([]byte, error) {
panic("TODO")
}
func (StrangeStackItem) TryInteger() (*big.Int, error) {
panic("TODO")
}
func (StrangeStackItem) Equals(stackitem.Item) bool {
panic("TODO")
}
func (StrangeStackItem) Convert(stackitem.Type) (stackitem.Item, error) {
panic("TODO")
}
type ConvertibleStruct struct {
SomeInt int
SomeString string
err error
}
var _ = stackitem.Convertible(&ConvertibleStruct{})
func (s *ConvertibleStruct) ToStackItem() (stackitem.Item, error) {
if s.err != nil {
return nil, s.err
}
return stackitem.NewArray([]stackitem.Item{
stackitem.Make(s.SomeInt),
stackitem.Make(s.SomeString),
}), nil
}
func (s *ConvertibleStruct) FromStackItem(si stackitem.Item) error {
panic("TODO")
}
func TestEmitConvertible(t *testing.T) {
t.Run("good", func(t *testing.T) {
buf := io.NewBufBinWriter()
str := &ConvertibleStruct{
SomeInt: 5,
SomeString: "str",
}
Convertible(buf.BinWriter, str)
require.NoError(t, buf.Err)
res := buf.Bytes()
// The struct itself:
assert.EqualValues(t, opcode.PUSHDATA1, res[0])
assert.EqualValues(t, 3, res[1])
assert.EqualValues(t, []byte("str"), res[2:5])
assert.EqualValues(t, opcode.PUSH5, res[5])
assert.EqualValues(t, opcode.PUSH2, res[6])
assert.EqualValues(t, opcode.PACK, res[7])
// Overall length:
assert.EqualValues(t, 8, len(res))
})
t.Run("error on conversion", func(t *testing.T) {
buf := io.NewBufBinWriter()
expectedErr := errors.New("error on conversion")
str := &ConvertibleStruct{
err: expectedErr,
}
Convertible(buf.BinWriter, str)
actualErr := buf.Err
require.Error(t, actualErr)
require.ErrorIs(t, actualErr, expectedErr)
})
}