diff --git a/pkg/core/native/native_test/notary_test.go b/pkg/core/native/native_test/notary_test.go index c4a58ac71..1f846cd3e 100644 --- a/pkg/core/native/native_test/notary_test.go +++ b/pkg/core/native/native_test/notary_test.go @@ -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), ¬ary.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), ¬ary.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, ¬ary.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, ¬ary.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, ¬ary.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), ¬ary.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), ¬ary.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), ¬ary.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, ¬ary.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, ¬ary.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, ¬ary.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) diff --git a/pkg/rpcclient/nep11/base_test.go b/pkg/rpcclient/nep11/base_test.go index 9ba9b3aa2..0aacd9f73 100644 --- a/pkg/rpcclient/nep11/base_test.go +++ b/pkg/rpcclient/nep11/base_test.go @@ -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) } } diff --git a/pkg/rpcclient/nep11/divisible_test.go b/pkg/rpcclient/nep11/divisible_test.go index dd9a1e26d..84be9469b 100644 --- a/pkg/rpcclient/nep11/divisible_test.go +++ b/pkg/rpcclient/nep11/divisible_test.go @@ -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) } } diff --git a/pkg/rpcclient/nep17/nep17_test.go b/pkg/rpcclient/nep17/nep17_test.go index 0fe260cbd..ca9908646 100644 --- a/pkg/rpcclient/nep17/nep17_test.go +++ b/pkg/rpcclient/nep17/nep17_test.go @@ -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) }) } diff --git a/pkg/rpcclient/notary/contract.go b/pkg/rpcclient/notary/contract.go index 749003f1c..3ded21bc5 100644 --- a/pkg/rpcclient/notary/contract.go +++ b/pkg/rpcclient/notary/contract.go @@ -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 +} diff --git a/pkg/rpcclient/notary/contract_test.go b/pkg/rpcclient/notary/contract_test.go index 1e50f8db5..336c6ce88 100644 --- a/pkg/rpcclient/notary/contract_test.go +++ b/pkg/rpcclient/notary/contract_test.go @@ -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) + }) + } +} diff --git a/pkg/rpcclient/notary/doc_test.go b/pkg/rpcclient/notary/doc_test.go index 33e2524f8..fa6f6909a 100644 --- a/pkg/rpcclient/notary/doc_test.go +++ b/pkg/rpcclient/notary/doc_test.go @@ -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), ¬ary.OnNEP17PaymentData{Till: 10000000}) var depositOK bool // Wait for transaction to be persisted, either it gets in and we get diff --git a/pkg/smartcontract/builder.go b/pkg/smartcontract/builder.go index 6d5d03331..016dd5dd5 100644 --- a/pkg/smartcontract/builder.go +++ b/pkg/smartcontract/builder.go @@ -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...) } diff --git a/pkg/vm/emit/emit.go b/pkg/vm/emit/emit.go index b79fcdeac..4cae72f65 100644 --- a/pkg/vm/emit/emit.go +++ b/pkg/vm/emit/emit.go @@ -98,7 +98,16 @@ func bigInt(w *io.BinWriter, n *big.Int, trySmall bool) { w.WriteBytes(padRight(1<= 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)) diff --git a/pkg/vm/emit/emit_test.go b/pkg/vm/emit/emit_test.go index 63b35cfdb..d19c75962 100644 --- a/pkg/vm/emit/emit_test.go +++ b/pkg/vm/emit/emit_test.go @@ -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) + }) +}