From b00eb51c55d196741a60a2d682861fea76d0b3e6 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 18 Nov 2020 11:59:34 +0300 Subject: [PATCH 01/14] core: add P2PNotary designated role --- config/protocol.unit_testnet.yml | 1 + pkg/core/blockchain.go | 2 +- pkg/core/native/contract.go | 4 ++-- pkg/core/native/designate.go | 25 +++++++++++++++---------- pkg/core/native_designate_test.go | 17 ++++++++++++++++- 5 files changed, 35 insertions(+), 14 deletions(-) diff --git a/config/protocol.unit_testnet.yml b/config/protocol.unit_testnet.yml index c74b4168a..e63b288ee 100644 --- a/config/protocol.unit_testnet.yml +++ b/config/protocol.unit_testnet.yml @@ -17,6 +17,7 @@ ProtocolConfiguration: - 127.0.0.1:20336 VerifyBlocks: true VerifyTransactions: true + P2PSigExtensions: true ApplicationConfiguration: # LogPath could be set up in case you need stdout logs to some proper file. diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 878261691..d8a53a2e4 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -171,7 +171,7 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L subCh: make(chan interface{}), unsubCh: make(chan interface{}), - contracts: *native.NewContracts(), + contracts: *native.NewContracts(cfg.P2PSigExtensions), } if err := bc.init(); err != nil { diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index a32dbc827..b34c2578c 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -49,7 +49,7 @@ func (cs *Contracts) ByName(name string) interop.Contract { // NewContracts returns new set of native contracts with new GAS, NEO and Policy // contracts. -func NewContracts() *Contracts { +func NewContracts(p2pSigExtensionsEnabled bool) *Contracts { cs := new(Contracts) gas := newGAS() @@ -72,7 +72,7 @@ func NewContracts() *Contracts { cs.Oracle = oracle cs.Contracts = append(cs.Contracts, oracle) - desig := newDesignate() + desig := newDesignate(p2pSigExtensionsEnabled) desig.NEO = neo cs.Designate = desig cs.Oracle.Desig = desig diff --git a/pkg/core/native/designate.go b/pkg/core/native/designate.go index 7440d8213..1f426ec5c 100644 --- a/pkg/core/native/designate.go +++ b/pkg/core/native/designate.go @@ -27,6 +27,9 @@ type Designate struct { rolesChangedFlag atomic.Value oracles atomic.Value + + // p2pSigExtensionsEnabled defines whether the P2P signature extensions logic is relevant. + p2pSigExtensionsEnabled bool } type oraclesData struct { @@ -50,6 +53,7 @@ type Role byte const ( RoleStateValidator Role = 4 RoleOracle Role = 8 + RoleP2PNotary Role = 128 ) // Various errors. @@ -62,13 +66,14 @@ var ( ErrNoBlock = errors.New("no persisting block in the context") ) -func isValidRole(r Role) bool { - return r == RoleOracle || r == RoleStateValidator +func (s *Designate) isValidRole(r Role) bool { + return r == RoleOracle || r == RoleStateValidator || (s.p2pSigExtensionsEnabled && r == RoleP2PNotary) } -func newDesignate() *Designate { +func newDesignate(p2pSigExtensionsEnabled bool) *Designate { s := &Designate{ContractMD: *interop.NewContractMD(designateName)} s.ContractID = designateContractID + s.p2pSigExtensionsEnabled = p2pSigExtensionsEnabled desc := newDescriptor("getDesignatedByRole", smartcontract.ArrayType, manifest.NewParameter("role", smartcontract.IntegerType), @@ -121,7 +126,7 @@ func (s *Designate) Metadata() *interop.ContractMD { } func (s *Designate) getDesignatedByRole(ic *interop.Context, args []stackitem.Item) stackitem.Item { - r, ok := getRole(args[0]) + r, ok := s.getRole(args[0]) if !ok { panic(ErrInvalidRole) } @@ -154,7 +159,7 @@ func oracleHashFromNodes(nodes keys.PublicKeys) util.Uint160 { } func (s *Designate) getLastDesignatedHash(d dao.DAO, r Role) (util.Uint160, error) { - if !isValidRole(r) { + if !s.isValidRole(r) { return util.Uint160{}, ErrInvalidRole } if r == RoleOracle && !s.rolesChanged() { @@ -174,7 +179,7 @@ func (s *Designate) getLastDesignatedHash(d dao.DAO, r Role) (util.Uint160, erro // GetDesignatedByRole returns nodes for role r. func (s *Designate) GetDesignatedByRole(d dao.DAO, r Role, index uint32) (keys.PublicKeys, uint32, error) { - if !isValidRole(r) { + if !s.isValidRole(r) { return nil, 0, ErrInvalidRole } if r == RoleOracle && !s.rolesChanged() { @@ -214,7 +219,7 @@ func (s *Designate) GetDesignatedByRole(d dao.DAO, r Role, index uint32) (keys.P } func (s *Designate) designateAsRole(ic *interop.Context, args []stackitem.Item) stackitem.Item { - r, ok := getRole(args[0]) + r, ok := s.getRole(args[0]) if !ok { panic(ErrInvalidRole) } @@ -239,7 +244,7 @@ func (s *Designate) DesignateAsRole(ic *interop.Context, r Role, pubs keys.Publi if length > maxNodeCount { return ErrLargeNodeList } - if !isValidRole(r) { + if !s.isValidRole(r) { return ErrInvalidRole } h := s.NEO.GetCommitteeAddress() @@ -263,7 +268,7 @@ func (s *Designate) DesignateAsRole(ic *interop.Context, r Role, pubs keys.Publi return ic.DAO.PutStorageItem(s.ContractID, key, si) } -func getRole(item stackitem.Item) (Role, bool) { +func (s *Designate) getRole(item stackitem.Item) (Role, bool) { bi, err := item.TryInteger() if err != nil { return 0, false @@ -272,5 +277,5 @@ func getRole(item stackitem.Item) (Role, bool) { return 0, false } u := bi.Uint64() - return Role(u), u <= math.MaxUint8 && isValidRole(Role(u)) + return Role(u), u <= math.MaxUint8 && s.isValidRole(Role(u)) } diff --git a/pkg/core/native_designate_test.go b/pkg/core/native_designate_test.go index 4c4e78779..697821f00 100644 --- a/pkg/core/native_designate_test.go +++ b/pkg/core/native_designate_test.go @@ -163,7 +163,7 @@ func TestDesignate_DesignateAsRole(t *testing.T) { require.Equal(t, 0, len(pubs)) require.Equal(t, uint32(0), index) - // Set another role. + // Set StateValidator role. _, err = keys.NewPrivateKey() require.NoError(t, err) pub1 := priv.PublicKey() @@ -180,4 +180,19 @@ func TestDesignate_DesignateAsRole(t *testing.T) { require.NoError(t, err) require.Equal(t, keys.PublicKeys{pub1}, pubs) require.Equal(t, bl.Index+1, index) + + // Set P2PNotary role. + pubs, index, err = des.GetDesignatedByRole(ic.DAO, native.RoleP2PNotary, 255) + require.NoError(t, err) + require.Equal(t, 0, len(pubs)) + require.Equal(t, uint32(0), index) + + err = des.DesignateAsRole(ic, native.RoleP2PNotary, keys.PublicKeys{pub1}) + require.NoError(t, err) + require.NoError(t, des.OnPersistEnd(ic.DAO)) + + pubs, index, err = des.GetDesignatedByRole(ic.DAO, native.RoleP2PNotary, 255) + require.NoError(t, err) + require.Equal(t, keys.PublicKeys{pub1}, pubs) + require.Equal(t, bl.Index+1, index) } From 8548786444a400bd05203103ccf17a0b9aecfd72 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 18 Nov 2020 14:00:24 +0300 Subject: [PATCH 02/14] core: do not rewrite binreader error for bad Conflicts attr We should keep the original BinReader error untouched in case if it is exists. --- pkg/core/transaction/attribute_test.go | 28 +++++++++++++++++++------- pkg/core/transaction/conflicts.go | 6 +++++- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/pkg/core/transaction/attribute_test.go b/pkg/core/transaction/attribute_test.go index 935746422..6857dc08d 100644 --- a/pkg/core/transaction/attribute_test.go +++ b/pkg/core/transaction/attribute_test.go @@ -7,6 +7,8 @@ import ( "github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/internal/testserdes" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/util" "github.com/stretchr/testify/require" ) @@ -61,13 +63,25 @@ func TestAttribute_EncodeBinary(t *testing.T) { }) }) t.Run("Conflicts", func(t *testing.T) { - attr := &Attribute{ - Type: ConflictsT, - Value: &Conflicts{ - Hash: random.Uint256(), - }, - } - testserdes.EncodeDecodeBinary(t, attr, new(Attribute)) + t.Run("positive", func(t *testing.T) { + attr := &Attribute{ + Type: ConflictsT, + Value: &Conflicts{ + Hash: random.Uint256(), + }, + } + testserdes.EncodeDecodeBinary(t, attr, new(Attribute)) + }) + t.Run("negative: too long", func(t *testing.T) { + bw := io.NewBufBinWriter() + bw.WriteVarBytes(make([]byte, util.Uint256Size+1)) + require.Error(t, testserdes.DecodeBinary(bw.Bytes(), new(Conflicts))) + }) + t.Run("negative: bad uint256", func(t *testing.T) { + bw := io.NewBufBinWriter() + bw.WriteVarBytes(make([]byte, util.Uint256Size-1)) + require.Error(t, testserdes.DecodeBinary(bw.Bytes(), new(Conflicts))) + }) }) } diff --git a/pkg/core/transaction/conflicts.go b/pkg/core/transaction/conflicts.go index 235ed3341..daef11724 100644 --- a/pkg/core/transaction/conflicts.go +++ b/pkg/core/transaction/conflicts.go @@ -12,7 +12,11 @@ type Conflicts struct { // DecodeBinary implements io.Serializable interface. func (c *Conflicts) DecodeBinary(br *io.BinReader) { - hash, err := util.Uint256DecodeBytesBE(br.ReadVarBytes(util.Uint256Size)) + bytes := br.ReadVarBytes(util.Uint256Size) + if br.Err != nil { + return + } + hash, err := util.Uint256DecodeBytesBE(bytes) if err != nil { br.Err = err return From 31aa66a4a481170c9abfdab7cc698a6e12824f2e Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 18 Nov 2020 14:13:09 +0300 Subject: [PATCH 03/14] core: check the length of NotValidBefore attr while decoding DecodeBinary throws panic otherwise. --- pkg/core/transaction/attribute_test.go | 26 +++++++++++++++++------- pkg/core/transaction/not_valid_before.go | 8 ++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/pkg/core/transaction/attribute_test.go b/pkg/core/transaction/attribute_test.go index 6857dc08d..1474ebfca 100644 --- a/pkg/core/transaction/attribute_test.go +++ b/pkg/core/transaction/attribute_test.go @@ -31,13 +31,25 @@ func TestAttribute_EncodeBinary(t *testing.T) { testserdes.EncodeDecodeBinary(t, attr, new(Attribute)) }) t.Run("NotValidBefore", func(t *testing.T) { - attr := &Attribute{ - Type: NotValidBeforeT, - Value: &NotValidBefore{ - Height: 123, - }, - } - testserdes.EncodeDecodeBinary(t, attr, new(Attribute)) + t.Run("positive", func(t *testing.T) { + attr := &Attribute{ + Type: NotValidBeforeT, + Value: &NotValidBefore{ + Height: 123, + }, + } + testserdes.EncodeDecodeBinary(t, attr, new(Attribute)) + }) + t.Run("bad format: too long", func(t *testing.T) { + bw := io.NewBufBinWriter() + bw.WriteVarBytes([]byte{1, 2, 3, 4, 5}) + require.Error(t, testserdes.DecodeBinary(bw.Bytes(), new(NotValidBefore))) + }) + t.Run("bad format: too short", func(t *testing.T) { + bw := io.NewBufBinWriter() + bw.WriteVarBytes([]byte{1, 2, 3}) + require.Error(t, testserdes.DecodeBinary(bw.Bytes(), new(NotValidBefore))) + }) }) t.Run("Reserved", func(t *testing.T) { getReservedAttribute := func(t AttrType) *Attribute { diff --git a/pkg/core/transaction/not_valid_before.go b/pkg/core/transaction/not_valid_before.go index c1596a277..aa64b14c0 100644 --- a/pkg/core/transaction/not_valid_before.go +++ b/pkg/core/transaction/not_valid_before.go @@ -2,6 +2,7 @@ package transaction import ( "encoding/binary" + "fmt" "github.com/nspcc-dev/neo-go/pkg/io" ) @@ -14,6 +15,13 @@ type NotValidBefore struct { // DecodeBinary implements io.Serializable interface. func (n *NotValidBefore) DecodeBinary(br *io.BinReader) { bytes := br.ReadVarBytes(4) + if br.Err != nil { + return + } + if len(bytes) != 4 { + br.Err = fmt.Errorf("expected 4 bytes, got %d", len(bytes)) + return + } n.Height = binary.LittleEndian.Uint32(bytes) } From 8cdf2d3464286371bf3a54c02db0d2c81a4a05b2 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 20 Nov 2020 13:25:19 +0300 Subject: [PATCH 04/14] core: unify `verifyTxAttributes` errors We already have pretty ErrInvalidAttribute error, so I think that all other `verifyTxAttributes` errors should be wrappers around ErrInvalidAttr. --- pkg/core/blockchain.go | 11 +++++------ pkg/core/blockchain_test.go | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index d8a53a2e4..3d9c68d1a 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1254,7 +1254,6 @@ func (bc *Blockchain) verifyHeader(currHeader, prevHeader *block.Header) error { // Various errors that could be returned upon verification. var ( - ErrTxNotYetValid = errors.New("transaction is not yet valid") ErrTxExpired = errors.New("transaction has expired") ErrInsufficientFunds = errors.New("insufficient funds") ErrTxSmallNetworkFee = errors.New("too small network fee") @@ -1365,23 +1364,23 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error { } case transaction.NotValidBeforeT: if !bc.config.P2PSigExtensions { - return errors.New("NotValidBefore attribute was found, but P2PSigExtensions are disabled") + return fmt.Errorf("%w: NotValidBefore attribute was found, but P2PSigExtensions are disabled", ErrInvalidAttribute) } nvb := tx.Attributes[i].Value.(*transaction.NotValidBefore) if height := bc.BlockHeight(); height < nvb.Height { - return fmt.Errorf("%w: NotValidBefore = %d, current height = %d", ErrTxNotYetValid, nvb.Height, height) + return fmt.Errorf("%w: transaction is not yet valid: NotValidBefore = %d, current height = %d", ErrInvalidAttribute, nvb.Height, height) } case transaction.ConflictsT: if !bc.config.P2PSigExtensions { - return errors.New("Conflicts attribute was found, but P2PSigExtensions are disabled") + return fmt.Errorf("%w: Conflicts attribute was found, but P2PSigExtensions are disabled", ErrInvalidAttribute) } conflicts := tx.Attributes[i].Value.(*transaction.Conflicts) if err := bc.dao.HasTransaction(conflicts.Hash); errors.Is(err, dao.ErrAlreadyExists) { - return fmt.Errorf("conflicting transaction %s is already on chain", conflicts.Hash.StringLE()) + return fmt.Errorf("%w: conflicting transaction %s is already on chain", ErrInvalidAttribute, conflicts.Hash.StringLE()) } default: if !bc.config.ReservedAttributes && attrType >= transaction.ReservedLowerBound && attrType <= transaction.ReservedUpperBound { - return errors.New("attribute of reserved type was found, but ReservedAttributes are disabled") + return fmt.Errorf("%w: attribute of reserved type was found, but ReservedAttributes are disabled", ErrInvalidAttribute) } } } diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index d9dfa0e85..1a0b208a5 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -621,7 +621,7 @@ func TestVerifyTx(t *testing.T) { bc.config.P2PSigExtensions = true t.Run("NotYetValid", func(t *testing.T) { tx := getNVBTx(bc.blockHeight + 1) - require.True(t, errors.Is(bc.VerifyTx(tx), ErrTxNotYetValid)) + require.True(t, errors.Is(bc.VerifyTx(tx), ErrInvalidAttribute)) }) t.Run("positive", func(t *testing.T) { tx := getNVBTx(bc.blockHeight) From fc9f0034c9cf90bb6c36a05dd5f069014875dd43 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 20 Nov 2020 13:30:27 +0300 Subject: [PATCH 05/14] core: adjust `verifyTxAttributes` for HighPriority attributes The fact that they have hight priority does not mean that cheks for other attributes should be skipped. --- pkg/core/blockchain.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 3d9c68d1a..2c58a3a3a 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1328,12 +1328,9 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error { switch attrType := tx.Attributes[i].Type; attrType { case transaction.HighPriority: h := bc.contracts.NEO.GetCommitteeAddress() - for i := range tx.Signers { - if tx.Signers[i].Account.Equals(h) { - return nil - } + if !tx.HasSigner(h) { + return fmt.Errorf("%w: high priority tx is not signed by committee", ErrInvalidAttribute) } - return fmt.Errorf("%w: high priority tx is not signed by committee", ErrInvalidAttribute) case transaction.OracleResponseT: h, err := bc.contracts.Oracle.GetScriptHash(bc.dao) if err != nil || h.Equals(util.Uint160{}) { From 52cf3282962576869525069a953ae2f902308af2 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 18 Nov 2020 14:26:13 +0300 Subject: [PATCH 06/14] core: add NotaryAssisted transaction attribute --- pkg/core/blockchain.go | 11 ++++++ pkg/core/blockchain_test.go | 45 +++++++++++++++++++++++-- pkg/core/transaction/attribute.go | 7 +++- pkg/core/transaction/attribute_test.go | 32 +++++++++++++++++- pkg/core/transaction/attrtype.go | 1 + pkg/core/transaction/attrtype_string.go | 7 ++-- pkg/core/transaction/notary_assisted.go | 37 ++++++++++++++++++++ 7 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 pkg/core/transaction/notary_assisted.go diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 2c58a3a3a..bfe44143b 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1280,6 +1280,13 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool. return fmt.Errorf("%w: (%d > MaxTransactionSize %d)", ErrTxTooBig, size, transaction.MaxTransactionSize) } needNetworkFee := int64(size) * bc.FeePerByte() + if bc.P2PSigExtensionsEnabled() { + attrs := t.GetAttributes(transaction.NotaryAssistedT) + if len(attrs) != 0 { + na := attrs[0].Value.(*transaction.NotaryAssisted) + needNetworkFee += (int64(na.NKeys) + 1) * transaction.NotaryServiceFeePerKey + } + } netFee := t.NetworkFee - needNetworkFee if netFee < 0 { return fmt.Errorf("%w: net fee is %v, need %v", ErrTxSmallNetworkFee, t.NetworkFee, needNetworkFee) @@ -1375,6 +1382,10 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error { if err := bc.dao.HasTransaction(conflicts.Hash); errors.Is(err, dao.ErrAlreadyExists) { return fmt.Errorf("%w: conflicting transaction %s is already on chain", ErrInvalidAttribute, conflicts.Hash.StringLE()) } + case transaction.NotaryAssistedT: + if !bc.config.P2PSigExtensions { + return fmt.Errorf("%w: NotaryAssisted attribute was found, but P2PSigExtensions are disabled", ErrInvalidAttribute) + } default: if !bc.config.ReservedAttributes && attrType >= transaction.ReservedLowerBound && attrType <= transaction.ReservedUpperBound { return fmt.Errorf("%w: attribute of reserved type was found, but ReservedAttributes are disabled", ErrInvalidAttribute) diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 1a0b208a5..e771b0135 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -652,12 +652,12 @@ func TestVerifyTx(t *testing.T) { return tx } t.Run("Disabled", func(t *testing.T) { - tx := getReservedTx(transaction.ReservedLowerBound + 2) + tx := getReservedTx(transaction.ReservedLowerBound + 3) require.Error(t, bc.VerifyTx(tx)) }) t.Run("Enabled", func(t *testing.T) { bc.config.ReservedAttributes = true - tx := getReservedTx(transaction.ReservedLowerBound + 2) + tx := getReservedTx(transaction.ReservedLowerBound + 3) require.NoError(t, bc.VerifyTx(tx)) }) }) @@ -719,6 +719,47 @@ func TestVerifyTx(t *testing.T) { }) }) }) + t.Run("NotaryAssisted", func(t *testing.T) { + getNotaryAssistedTx := func(signaturesCount uint8, serviceFee int64) *transaction.Transaction { + tx := bc.newTestTx(h, testScript) + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{ + NKeys: signaturesCount, + }}) + tx.NetworkFee += serviceFee // additional fee for NotaryAssisted attribute + tx.NetworkFee += 4_000_000 // multisig check + tx.Signers = []transaction.Signer{{ + Account: testchain.CommitteeScriptHash(), + Scopes: transaction.None, + }} + rawScript := testchain.CommitteeVerificationScript() + require.NoError(t, err) + size := io.GetVarSize(tx) + netFee, sizeDelta := fee.Calculate(rawScript) + tx.NetworkFee += netFee + tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() + data := tx.GetSignedPart() + tx.Scripts = []transaction.Witness{{ + InvocationScript: testchain.SignCommittee(data), + VerificationScript: rawScript, + }} + return tx + } + t.Run("Disabled", func(t *testing.T) { + bc.config.P2PSigExtensions = false + tx := getNotaryAssistedTx(0, 0) + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("Enabled, insufficient network fee", func(t *testing.T) { + bc.config.P2PSigExtensions = true + tx := getNotaryAssistedTx(1, 0) + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("Enabled, positive", func(t *testing.T) { + bc.config.P2PSigExtensions = true + tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey) + require.NoError(t, bc.VerifyTx(tx)) + }) + }) }) } diff --git a/pkg/core/transaction/attribute.go b/pkg/core/transaction/attribute.go index 5ef14dd17..c81dbb1b5 100644 --- a/pkg/core/transaction/attribute.go +++ b/pkg/core/transaction/attribute.go @@ -39,6 +39,8 @@ func (attr *Attribute) DecodeBinary(br *io.BinReader) { attr.Value = new(NotValidBefore) case ConflictsT: attr.Value = new(Conflicts) + case NotaryAssistedT: + attr.Value = new(NotaryAssisted) default: if t >= ReservedLowerBound && t <= ReservedUpperBound { attr.Value = new(Reserved) @@ -55,7 +57,7 @@ func (attr *Attribute) EncodeBinary(bw *io.BinWriter) { bw.WriteB(byte(attr.Type)) switch t := attr.Type; t { case HighPriority: - case OracleResponseT, NotValidBeforeT, ConflictsT: + case OracleResponseT, NotValidBeforeT, ConflictsT, NotaryAssistedT: attr.Value.EncodeBinary(bw) default: if t >= ReservedLowerBound && t <= ReservedUpperBound { @@ -97,6 +99,9 @@ func (attr *Attribute) UnmarshalJSON(data []byte) error { case ConflictsT.String(): attr.Type = ConflictsT attr.Value = new(Conflicts) + case NotaryAssistedT.String(): + attr.Type = NotaryAssistedT + attr.Value = new(NotaryAssisted) default: return errors.New("wrong Type") } diff --git a/pkg/core/transaction/attribute_test.go b/pkg/core/transaction/attribute_test.go index 1474ebfca..4d43df95f 100644 --- a/pkg/core/transaction/attribute_test.go +++ b/pkg/core/transaction/attribute_test.go @@ -61,7 +61,7 @@ func TestAttribute_EncodeBinary(t *testing.T) { } } t.Run("lower bound", func(t *testing.T) { - testserdes.EncodeDecodeBinary(t, getReservedAttribute(ReservedLowerBound+2), new(Attribute)) + testserdes.EncodeDecodeBinary(t, getReservedAttribute(ReservedLowerBound+3), new(Attribute)) }) t.Run("upper bound", func(t *testing.T) { testserdes.EncodeDecodeBinary(t, getReservedAttribute(ReservedUpperBound), new(Attribute)) @@ -95,6 +95,27 @@ func TestAttribute_EncodeBinary(t *testing.T) { require.Error(t, testserdes.DecodeBinary(bw.Bytes(), new(Conflicts))) }) }) + t.Run("NotaryAssisted", func(t *testing.T) { + t.Run("positive", func(t *testing.T) { + attr := &Attribute{ + Type: NotaryAssistedT, + Value: &NotaryAssisted{ + NKeys: 3, + }, + } + testserdes.EncodeDecodeBinary(t, attr, new(Attribute)) + }) + t.Run("bad format: too long", func(t *testing.T) { + bw := io.NewBufBinWriter() + bw.WriteVarBytes(make([]byte, 2)) + require.Error(t, testserdes.DecodeBinary(bw.Bytes(), new(NotaryAssisted))) + }) + t.Run("bad format: too short", func(t *testing.T) { + bw := io.NewBufBinWriter() + bw.WriteVarBytes([]byte{}) + require.Error(t, testserdes.DecodeBinary(bw.Bytes(), new(NotaryAssisted))) + }) + }) } func TestAttribute_MarshalJSON(t *testing.T) { @@ -149,4 +170,13 @@ func TestAttribute_MarshalJSON(t *testing.T) { } testserdes.MarshalUnmarshalJSON(t, attr, new(Attribute)) }) + t.Run("NotaryAssisted", func(t *testing.T) { + attr := &Attribute{ + Type: NotaryAssistedT, + Value: &NotaryAssisted{ + NKeys: 3, + }, + } + testserdes.MarshalUnmarshalJSON(t, attr, new(Attribute)) + }) } diff --git a/pkg/core/transaction/attrtype.go b/pkg/core/transaction/attrtype.go index fa68ae5cc..c6879fefa 100644 --- a/pkg/core/transaction/attrtype.go +++ b/pkg/core/transaction/attrtype.go @@ -18,6 +18,7 @@ const ( OracleResponseT AttrType = 0x11 // OracleResponse NotValidBeforeT AttrType = ReservedLowerBound // NotValidBefore ConflictsT AttrType = ReservedLowerBound + 1 // Conflicts + NotaryAssistedT AttrType = ReservedLowerBound + 2 // NotaryAssisted ) func (a AttrType) allowMultiple() bool { diff --git a/pkg/core/transaction/attrtype_string.go b/pkg/core/transaction/attrtype_string.go index 70db86618..1f2eb06ba 100644 --- a/pkg/core/transaction/attrtype_string.go +++ b/pkg/core/transaction/attrtype_string.go @@ -12,16 +12,17 @@ func _() { _ = x[OracleResponseT-17] _ = x[NotValidBeforeT-224] _ = x[ConflictsT-225] + _ = x[NotaryAssistedT-226] } const ( _AttrType_name_0 = "HighPriority" _AttrType_name_1 = "OracleResponse" - _AttrType_name_2 = "NotValidBeforeConflicts" + _AttrType_name_2 = "NotValidBeforeConflictsNotaryAssisted" ) var ( - _AttrType_index_2 = [...]uint8{0, 14, 23} + _AttrType_index_2 = [...]uint8{0, 14, 23, 37} ) func (i AttrType) String() string { @@ -30,7 +31,7 @@ func (i AttrType) String() string { return _AttrType_name_0 case i == 17: return _AttrType_name_1 - case 224 <= i && i <= 225: + case 224 <= i && i <= 226: i -= 224 return _AttrType_name_2[_AttrType_index_2[i]:_AttrType_index_2[i+1]] default: diff --git a/pkg/core/transaction/notary_assisted.go b/pkg/core/transaction/notary_assisted.go new file mode 100644 index 000000000..119dc6a3b --- /dev/null +++ b/pkg/core/transaction/notary_assisted.go @@ -0,0 +1,37 @@ +package transaction + +import ( + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/io" +) + +// NotaryServiceFeePerKey is a reward per key for notary nodes. +const NotaryServiceFeePerKey = 1000_0000 // 0.1 GAS + +// NotaryAssisted represents attribute for notary service transactions. +type NotaryAssisted struct { + NKeys uint8 `json:"nkeys"` +} + +// DecodeBinary implements io.Serializable interface. +func (n *NotaryAssisted) DecodeBinary(br *io.BinReader) { + bytes := br.ReadVarBytes(1) + if br.Err != nil { + return + } + if len(bytes) != 1 { + br.Err = fmt.Errorf("expected 1 byte, got %d", len(bytes)) + return + } + n.NKeys = bytes[0] +} + +// EncodeBinary implements io.Serializable interface. +func (n *NotaryAssisted) EncodeBinary(w *io.BinWriter) { + w.WriteVarBytes([]byte{n.NKeys}) +} + +func (n *NotaryAssisted) toJSONMap(m map[string]interface{}) { + m["nkeys"] = n.NKeys +} From 97069a05d5688b2d21b7d3799bacd5f048e9d5cb Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 18 Nov 2020 18:27:06 +0300 Subject: [PATCH 07/14] core: fix locking in storeBlock --- pkg/core/blockchain.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index bfe44143b..d0c0a4007 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -705,9 +705,11 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error return err } if err := bc.contracts.Policy.OnPersistEnd(bc.dao); err != nil { + bc.lock.Unlock() return fmt.Errorf("failed to call OnPersistEnd for Policy native contract: %w", err) } if err := bc.contracts.Designate.OnPersistEnd(bc.dao); err != nil { + bc.lock.Unlock() return err } bc.dao.MPT.Flush() From 2fee69f26f75b3dbbf5ea9538f442736b857adf2 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 19 Nov 2020 18:40:36 +0300 Subject: [PATCH 08/14] core: add missing onPersist and postPersist methods to natives Although not every contract is persisted (see https://github.com/neo-project/neo/blob/master/src/neo/Ledger/Blockchain.cs#L94) we have to add `onPersist` and `postPersist` methods to every native contract in order to display them in manifest for users and follow C# behaviour. In C# there are `onPersist` and `postPersist` methods in base native contract class, see https://github.com/neo-project/neo/blob/master/src/neo/SmartContract/Native/NativeContract.cs#L141 and https://github.com/neo-project/neo/blob/master/src/neo/SmartContract/Native/NativeContract.cs#L148 --- pkg/core/native/contract.go | 7 +++++++ pkg/core/native/designate.go | 8 ++++++++ pkg/core/native/native_gas.go | 2 +- pkg/core/native/native_neo.go | 2 +- pkg/core/native/native_nep17.go | 10 +--------- pkg/core/native/oracle.go | 4 ++++ 6 files changed, 22 insertions(+), 11 deletions(-) diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index b34c2578c..007ff8a1e 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -133,3 +133,10 @@ func postPersistBase(ic *interop.Context) error { } return nil } + +func onPersistBase(ic *interop.Context) error { + if ic.Trigger != trigger.OnPersist { + return errors.New("onPersist must be trigered by system") + } + return nil +} diff --git a/pkg/core/native/designate.go b/pkg/core/native/designate.go index 1f426ec5c..5eeb2e15a 100644 --- a/pkg/core/native/designate.go +++ b/pkg/core/native/designate.go @@ -91,6 +91,14 @@ func newDesignate(p2pSigExtensionsEnabled bool) *Designate { md = newMethodAndPrice(nameMethod(designateName), 0, smartcontract.NoneFlag) s.AddMethod(md, desc, true) + desc = newDescriptor("onPersist", smartcontract.VoidType) + md = newMethodAndPrice(getOnPersistWrapper(onPersistBase), 0, smartcontract.AllowModifyStates) + s.AddMethod(md, desc, false) + + desc = newDescriptor("postPersist", smartcontract.VoidType) + md = newMethodAndPrice(getOnPersistWrapper(postPersistBase), 0, smartcontract.AllowModifyStates) + s.AddMethod(md, desc, false) + return s } diff --git a/pkg/core/native/native_gas.go b/pkg/core/native/native_gas.go index 3a7dde827..867e8aa76 100644 --- a/pkg/core/native/native_gas.go +++ b/pkg/core/native/native_gas.go @@ -31,7 +31,7 @@ func newGAS() *GAS { nep17.symbol = "gas" nep17.decimals = 8 nep17.factor = GASFactor - nep17.onPersist = chainOnPersist(nep17.OnPersist, g.OnPersist) + nep17.onPersist = chainOnPersist(onPersistBase, g.OnPersist) nep17.incBalance = g.increaseBalance nep17.ContractID = gasContractID diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 76004775b..45bd97f8b 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -97,7 +97,7 @@ func newNEO() *NEO { nep17.symbol = "neo" nep17.decimals = 0 nep17.factor = 1 - nep17.onPersist = chainOnPersist(nep17.OnPersist, n.OnPersist) + nep17.onPersist = chainOnPersist(onPersistBase, n.OnPersist) nep17.postPersist = chainOnPersist(nep17.postPersist, n.PostPersist) nep17.incBalance = n.increaseBalance nep17.ContractID = neoContractID diff --git a/pkg/core/native/native_nep17.go b/pkg/core/native/native_nep17.go index 84216e81c..5ebf99a1e 100644 --- a/pkg/core/native/native_nep17.go +++ b/pkg/core/native/native_nep17.go @@ -13,7 +13,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" @@ -85,7 +84,7 @@ func newNEP17Native(name string) *nep17TokenNative { n.AddMethod(md, desc, false) desc = newDescriptor("onPersist", smartcontract.VoidType) - md = newMethodAndPrice(getOnPersistWrapper(n.OnPersist), 0, smartcontract.AllowModifyStates) + md = newMethodAndPrice(getOnPersistWrapper(onPersistBase), 0, smartcontract.AllowModifyStates) n.AddMethod(md, desc, false) desc = newDescriptor("postPersist", smartcontract.VoidType) @@ -287,13 +286,6 @@ func (c *nep17TokenNative) addTokens(ic *interop.Context, h util.Uint160, amount } } -func (c *nep17TokenNative) OnPersist(ic *interop.Context) error { - if ic.Trigger != trigger.OnPersist { - return errors.New("onPersist must be triggerred by system") - } - return nil -} - func newDescriptor(name string, ret smartcontract.ParamType, ps ...manifest.Parameter) *manifest.Method { return &manifest.Method{ Name: name, diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go index d94f6a693..d90f8d508 100644 --- a/pkg/core/native/oracle.go +++ b/pkg/core/native/oracle.go @@ -137,6 +137,10 @@ func newOracle() *Oracle { md = newMethodAndPrice(getOnPersistWrapper(pp), 0, smartcontract.AllowModifyStates) o.AddMethod(md, desc, false) + desc = newDescriptor("onPersist", smartcontract.VoidType) + md = newMethodAndPrice(getOnPersistWrapper(onPersistBase), 0, smartcontract.AllowModifyStates) + o.AddMethod(md, desc, false) + o.AddEvent("OracleRequest", manifest.NewParameter("Id", smartcontract.IntegerType), manifest.NewParameter("RequestContract", smartcontract.Hash160Type), manifest.NewParameter("Url", smartcontract.StringType), From eca27055b854fe115197cdd8dca1ff6a1f41dc6c Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 25 Nov 2020 12:44:49 +0300 Subject: [PATCH 09/14] core: fix NEP17 Transfer event `Transfer` event declaration was placed at the wrong part of `newNEP17Native`, that's why it had incorrect parameters. Fixed. --- pkg/core/native/native_nep17.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/core/native/native_nep17.go b/pkg/core/native/native_nep17.go index 5ebf99a1e..c5853503d 100644 --- a/pkg/core/native/native_nep17.go +++ b/pkg/core/native/native_nep17.go @@ -74,11 +74,13 @@ func newNEP17Native(name string) *nep17TokenNative { md = newMethodAndPrice(n.balanceOf, 1000000, smartcontract.AllowStates) n.AddMethod(md, desc, true) - desc = newDescriptor("transfer", smartcontract.BoolType, + transferParams := []manifest.Parameter{ manifest.NewParameter("from", smartcontract.Hash160Type), manifest.NewParameter("to", smartcontract.Hash160Type), manifest.NewParameter("amount", smartcontract.IntegerType), - manifest.NewParameter("data", smartcontract.AnyType), + } + desc = newDescriptor("transfer", smartcontract.BoolType, + append(transferParams, manifest.NewParameter("data", smartcontract.AnyType))..., ) md = newMethodAndPrice(n.Transfer, 8000000, smartcontract.AllowModifyStates) n.AddMethod(md, desc, false) @@ -91,7 +93,7 @@ func newNEP17Native(name string) *nep17TokenNative { md = newMethodAndPrice(getOnPersistWrapper(postPersistBase), 0, smartcontract.AllowModifyStates) n.AddMethod(md, desc, false) - n.AddEvent("Transfer", desc.Parameters...) + n.AddEvent("Transfer", transferParams...) return n } From c9acc43023368fa40868579e8b8fc938b61ffa9a Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 24 Nov 2020 12:08:25 +0300 Subject: [PATCH 10/14] core: invoke contract verification script with AllowStates flag We should call contract's `verify` with AllowStates flag. --- pkg/core/blockchain.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index d0c0a4007..6a3aeef5d 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1590,6 +1590,7 @@ func (bc *Blockchain) initVerificationVM(ic *interop.Context, hash util.Uint160, var isNative bool var initMD *manifest.Method verification := witness.VerificationScript + flags := smartcontract.NoneFlag if len(verification) != 0 { if witness.ScriptHash() != hash { return ErrWitnessHashMismatch @@ -1610,10 +1611,11 @@ func (bc *Blockchain) initVerificationVM(ic *interop.Context, hash util.Uint160, offset = md.Offset initMD = cs.Manifest.ABI.GetMethod(manifest.MethodInit) isNative = cs.ID < 0 + flags = smartcontract.AllowStates } v := ic.VM - v.LoadScriptWithFlags(verification, smartcontract.NoneFlag) + v.LoadScriptWithFlags(verification, flags) v.Jump(v.Context(), offset) if isNative { w := io.NewBufBinWriter() From 0f68528095b9fb7c3a1ad72875e31d4eae237258 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 24 Nov 2020 15:45:14 +0300 Subject: [PATCH 11/14] core: add callback to VM context --- pkg/core/interop/contract/call.go | 5 +++-- pkg/core/interop_neo.go | 2 +- pkg/core/interop_system_test.go | 4 ++-- pkg/core/native/native_nep17.go | 2 +- pkg/core/native_contract_test.go | 2 +- pkg/vm/context.go | 8 ++++++++ pkg/vm/vm.go | 3 +++ 7 files changed, 19 insertions(+), 7 deletions(-) diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index 4d757049e..d40878e6e 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -52,12 +52,12 @@ func callExInternal(ic *interop.Context, h []byte, name string, args []stackitem return errors.New("disallowed method call") } } - return CallExInternal(ic, cs, name, args, f, vm.EnsureNotEmpty) + return CallExInternal(ic, cs, name, args, f, vm.EnsureNotEmpty, nil) } // CallExInternal calls a contract with flags and can't be invoked directly by user. func CallExInternal(ic *interop.Context, cs *state.Contract, - name string, args []stackitem.Item, f smartcontract.CallFlag, checkReturn vm.CheckReturnState) error { + name string, args []stackitem.Item, f smartcontract.CallFlag, checkReturn vm.CheckReturnState, callback func(ctx *vm.Context)) error { md := cs.Manifest.ABI.GetMethod(name) if md == nil { return fmt.Errorf("method '%s' not found", name) @@ -88,6 +88,7 @@ func CallExInternal(ic *interop.Context, cs *state.Contract, ic.VM.Jump(ic.VM.Context(), md.Offset) } ic.VM.Context().CheckReturn = checkReturn + ic.VM.Context().Callback = callback md = cs.Manifest.ABI.GetMethod(manifest.MethodInit) if md != nil { diff --git a/pkg/core/interop_neo.go b/pkg/core/interop_neo.go index 35f9e9f0e..4e143756a 100644 --- a/pkg/core/interop_neo.go +++ b/pkg/core/interop_neo.go @@ -195,7 +195,7 @@ func callDeploy(ic *interop.Context, cs *state.Contract, isUpdate bool) error { md := cs.Manifest.ABI.GetMethod(manifest.MethodDeploy) if md != nil { return contract.CallExInternal(ic, cs, manifest.MethodDeploy, - []stackitem.Item{stackitem.NewBool(isUpdate)}, smartcontract.All, vm.EnsureIsEmpty) + []stackitem.Item{stackitem.NewBool(isUpdate)}, smartcontract.All, vm.EnsureIsEmpty, nil) } return nil } diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_test.go index 5f0be0574..42802165b 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_test.go @@ -911,7 +911,7 @@ func TestContractCreateDeploy(t *testing.T) { require.NoError(t, ic.VM.Run()) v.LoadScriptWithFlags(currCs.Script, smartcontract.All) - err := contract.CallExInternal(ic, cs, "getValue", nil, smartcontract.All, vm.EnsureNotEmpty) + err := contract.CallExInternal(ic, cs, "getValue", nil, smartcontract.All, vm.EnsureNotEmpty, nil) require.NoError(t, err) require.NoError(t, v.Run()) require.Equal(t, "create", v.Estack().Pop().String()) @@ -932,7 +932,7 @@ func TestContractCreateDeploy(t *testing.T) { require.NoError(t, v.Run()) v.LoadScriptWithFlags(currCs.Script, smartcontract.All) - err = contract.CallExInternal(ic, newCs, "getValue", nil, smartcontract.All, vm.EnsureNotEmpty) + err = contract.CallExInternal(ic, newCs, "getValue", nil, smartcontract.All, vm.EnsureNotEmpty, nil) require.NoError(t, err) require.NoError(t, v.Run()) require.Equal(t, "update", v.Estack().Pop().String()) diff --git a/pkg/core/native/native_nep17.go b/pkg/core/native/native_nep17.go index c5853503d..aa18d7ed7 100644 --- a/pkg/core/native/native_nep17.go +++ b/pkg/core/native/native_nep17.go @@ -161,7 +161,7 @@ func (c *nep17TokenNative) postTransfer(ic *interop.Context, from, to *util.Uint stackitem.NewBigInteger(amount), data, } - if err := contract.CallExInternal(ic, cs, manifest.MethodOnPayment, args, smartcontract.All, vm.EnsureIsEmpty); err != nil { + if err := contract.CallExInternal(ic, cs, manifest.MethodOnPayment, args, smartcontract.All, vm.EnsureIsEmpty, nil); err != nil { panic(err) } } diff --git a/pkg/core/native_contract_test.go b/pkg/core/native_contract_test.go index ac0528f91..67ced4b50 100644 --- a/pkg/core/native_contract_test.go +++ b/pkg/core/native_contract_test.go @@ -133,7 +133,7 @@ func (tn *testNative) call(ic *interop.Context, args []stackitem.Item, retState if err != nil { panic(err) } - err = contract.CallExInternal(ic, cs, string(bs), args[2].Value().([]stackitem.Item), smartcontract.All, retState) + err = contract.CallExInternal(ic, cs, string(bs), args[2].Value().([]stackitem.Item), smartcontract.All, retState, nil) if err != nil { panic(err) } diff --git a/pkg/vm/context.go b/pkg/vm/context.go index 4017f7516..08d38b87a 100644 --- a/pkg/vm/context.go +++ b/pkg/vm/context.go @@ -45,6 +45,14 @@ type Context struct { // Call flags this context was created with. callFlag smartcontract.CallFlag + // InvocationState contains expected return type and actions to be performed on context unload. + InvocationState +} + +// InvocationState contains return convention and callback to be executed on context unload. +type InvocationState struct { + // Callback is executed on context unload. + Callback func(ctx *Context) // CheckReturn specifies if amount of return values needs to be checked. CheckReturn CheckReturnState } diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 31a3197a0..03190378e 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -1410,6 +1410,9 @@ func (v *VM) unloadContext(ctx *Context) { if ctx.static != nil && currCtx != nil && ctx.static != currCtx.static { ctx.static.Clear() } + if ctx.Callback != nil { + ctx.Callback(ctx) + } switch ctx.CheckReturn { case NoCheck: case EnsureIsEmpty: From c013522296c918601ad74b138cb9ddbd903a4577 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 25 Nov 2020 12:36:38 +0300 Subject: [PATCH 12/14] core: remove native contracts' `name` method We have `name` in contract manifest. --- pkg/core/native/designate.go | 4 ---- pkg/core/native/native_nep17.go | 8 ++------ pkg/core/native/oracle.go | 4 ---- pkg/core/native/policy.go | 4 ---- pkg/core/native/util.go | 8 -------- pkg/core/native_contract_test.go | 25 ------------------------- 6 files changed, 2 insertions(+), 51 deletions(-) diff --git a/pkg/core/native/designate.go b/pkg/core/native/designate.go index 5eeb2e15a..ba3ce7cf1 100644 --- a/pkg/core/native/designate.go +++ b/pkg/core/native/designate.go @@ -87,10 +87,6 @@ func newDesignate(p2pSigExtensionsEnabled bool) *Designate { md = newMethodAndPrice(s.designateAsRole, 0, smartcontract.AllowModifyStates) s.AddMethod(md, desc, false) - desc = newDescriptor("name", smartcontract.StringType) - md = newMethodAndPrice(nameMethod(designateName), 0, smartcontract.NoneFlag) - s.AddMethod(md, desc, true) - desc = newDescriptor("onPersist", smartcontract.VoidType) md = newMethodAndPrice(getOnPersistWrapper(onPersistBase), 0, smartcontract.AllowModifyStates) s.AddMethod(md, desc, false) diff --git a/pkg/core/native/native_nep17.go b/pkg/core/native/native_nep17.go index aa18d7ed7..ddb24479a 100644 --- a/pkg/core/native/native_nep17.go +++ b/pkg/core/native/native_nep17.go @@ -53,12 +53,8 @@ func newNEP17Native(name string) *nep17TokenNative { n := &nep17TokenNative{ContractMD: *interop.NewContractMD(name)} n.Manifest.SupportedStandards = []string{manifest.NEP17StandardName} - desc := newDescriptor("name", smartcontract.StringType) - md := newMethodAndPrice(nameMethod(name), 0, smartcontract.NoneFlag) - n.AddMethod(md, desc, true) - - desc = newDescriptor("symbol", smartcontract.StringType) - md = newMethodAndPrice(n.Symbol, 0, smartcontract.NoneFlag) + desc := newDescriptor("symbol", smartcontract.StringType) + md := newMethodAndPrice(n.Symbol, 0, smartcontract.NoneFlag) n.AddMethod(md, desc, true) desc = newDescriptor("decimals", smartcontract.IntegerType) diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go index d90f8d508..81ed06f5f 100644 --- a/pkg/core/native/oracle.go +++ b/pkg/core/native/oracle.go @@ -113,10 +113,6 @@ func newOracle() *Oracle { md := newMethodAndPrice(o.request, oracleRequestPrice, smartcontract.AllowModifyStates) o.AddMethod(md, desc, false) - desc = newDescriptor("name", smartcontract.StringType) - md = newMethodAndPrice(nameMethod(oracleName), 0, smartcontract.NoneFlag) - o.AddMethod(md, desc, true) - desc = newDescriptor("finish", smartcontract.VoidType) md = newMethodAndPrice(o.finish, 0, smartcontract.AllowModifyStates) o.AddMethod(md, desc, false) diff --git a/pkg/core/native/policy.go b/pkg/core/native/policy.go index f6a9f6395..bcab99498 100644 --- a/pkg/core/native/policy.go +++ b/pkg/core/native/policy.go @@ -126,10 +126,6 @@ func newPolicy() *Policy { md = newMethodAndPrice(p.unblockAccount, 3000000, smartcontract.AllowModifyStates) p.AddMethod(md, desc, false) - desc = newDescriptor("name", smartcontract.StringType) - md = newMethodAndPrice(nameMethod(policyName), 0, smartcontract.NoneFlag) - p.AddMethod(md, desc, true) - desc = newDescriptor("onPersist", smartcontract.VoidType) md = newMethodAndPrice(getOnPersistWrapper(p.OnPersist), 0, smartcontract.AllowModifyStates) p.AddMethod(md, desc, false) diff --git a/pkg/core/native/util.go b/pkg/core/native/util.go index 97e02fc14..293581c34 100644 --- a/pkg/core/native/util.go +++ b/pkg/core/native/util.go @@ -2,10 +2,8 @@ package native import ( "github.com/nspcc-dev/neo-go/pkg/core/dao" - "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/io" - "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) func getSerializableFromDAO(id int32, d dao.DAO, key []byte, item io.Serializable) error { @@ -17,9 +15,3 @@ func getSerializableFromDAO(id int32, d dao.DAO, key []byte, item io.Serializabl item.DecodeBinary(r) return r.Err } - -func nameMethod(name string) interop.Method { - return func(_ *interop.Context, _ []stackitem.Item) stackitem.Item { - return stackitem.NewByteArray([]byte(name)) - } -} diff --git a/pkg/core/native_contract_test.go b/pkg/core/native_contract_test.go index 67ced4b50..a2b2ab497 100644 --- a/pkg/core/native_contract_test.go +++ b/pkg/core/native_contract_test.go @@ -276,28 +276,3 @@ func TestNativeContract_InvokeOtherContract(t *testing.T) { require.Equal(t, stackitem.Null{}, res[0].Stack[0]) // simple call is done with EnsureNotEmpty }) } - -func TestAllContractsHaveName(t *testing.T) { - bc := newTestChain(t) - defer bc.Close() - for _, c := range bc.contracts.Contracts { - name := c.Metadata().Name - t.Run(name, func(t *testing.T) { - w := io.NewBufBinWriter() - emit.AppCallWithOperationAndArgs(w.BinWriter, c.Metadata().Hash, "name") - require.NoError(t, w.Err) - - tx := transaction.New(netmode.UnitTestNet, w.Bytes(), 1015570) - tx.ValidUntilBlock = bc.blockHeight + 1 - addSigners(tx) - require.NoError(t, testchain.SignTx(bc, tx)) - require.NoError(t, bc.AddBlock(bc.newBlock(tx))) - - aers, err := bc.GetAppExecResults(tx.Hash(), trigger.Application) - require.NoError(t, err) - require.Equal(t, 1, len(aers)) - require.Len(t, aers[0].Stack, 1) - require.Equal(t, []byte(name), aers[0].Stack[0].Value()) - }) - } -} From 17842dabd6f0271d20217006e3651e28900063df Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 19 Nov 2020 13:00:46 +0300 Subject: [PATCH 13/14] core: implement native Notary contract --- pkg/core/blockchain.go | 3 + pkg/core/blockchain_test.go | 182 +++++++++++++++- pkg/core/helper_test.go | 61 +++++- pkg/core/native/contract.go | 18 +- pkg/core/native/native_nep17.go | 13 ++ pkg/core/native/notary.go | 357 ++++++++++++++++++++++++++++++++ pkg/core/native/util.go | 12 ++ pkg/core/native_notary_test.go | 298 ++++++++++++++++++++++++++ pkg/core/native_policy_test.go | 94 +++------ pkg/core/state/deposit.go | 26 +++ 10 files changed, 981 insertions(+), 83 deletions(-) create mode 100644 pkg/core/native/notary.go create mode 100644 pkg/core/native_notary_test.go create mode 100644 pkg/core/state/deposit.go diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 6a3aeef5d..9939cdf6f 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1388,6 +1388,9 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error { if !bc.config.P2PSigExtensions { return fmt.Errorf("%w: NotaryAssisted attribute was found, but P2PSigExtensions are disabled", ErrInvalidAttribute) } + if !tx.HasSigner(bc.contracts.Notary.Hash) { + return fmt.Errorf("%w: NotaryAssisted attribute was found, but transaction is not signed by the Notary native contract", ErrInvalidAttribute) + } default: if !bc.config.ReservedAttributes && attrType >= transaction.ReservedLowerBound && attrType <= transaction.ReservedUpperBound { return fmt.Errorf("%w: attribute of reserved type was found, but ReservedAttributes are disabled", ErrInvalidAttribute) diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index e771b0135..c32467401 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -286,7 +286,7 @@ func TestVerifyTx(t *testing.T) { require.Equal(t, 1, len(aer)) require.Equal(t, aer[0].VMState, vm.HaltState) - res, err := invokeNativePolicyMethod(bc, "blockAccount", accs[1].PrivateKey().GetScriptHash().BytesBE()) + res, err := invokeContractMethod(bc, 100000000, bc.contracts.Policy.Hash, "blockAccount", accs[1].PrivateKey().GetScriptHash().BytesBE()) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) @@ -720,6 +720,23 @@ func TestVerifyTx(t *testing.T) { }) }) t.Run("NotaryAssisted", func(t *testing.T) { + notary, err := wallet.NewAccount() + require.NoError(t, err) + txSetNotary := transaction.New(netmode.UnitTestNet, []byte{}, 0) + setSigner(txSetNotary, testchain.CommitteeScriptHash()) + txSetNotary.Scripts = []transaction.Witness{{ + InvocationScript: testchain.SignCommittee(txSetNotary.GetSignedPart()), + VerificationScript: testchain.CommitteeVerificationScript(), + }} + bl := block.New(netmode.UnitTestNet, false) + bl.Index = bc.BlockHeight() + 1 + ic := bc.newInteropContext(trigger.All, bc.dao, bl, txSetNotary) + ic.SpawnVM() + ic.VM.LoadScript([]byte{byte(opcode.RET)}) + require.NoError(t, bc.contracts.Designate.DesignateAsRole(ic, native.RoleP2PNotary, keys.PublicKeys{notary.PrivateKey().PublicKey()})) + require.NoError(t, bc.contracts.Designate.OnPersistEnd(ic.DAO)) + _, err = ic.DAO.Persist() + require.NoError(t, err) getNotaryAssistedTx := func(signaturesCount uint8, serviceFee int64) *transaction.Transaction { tx := bc.newTestTx(h, testScript) tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{ @@ -730,34 +747,177 @@ func TestVerifyTx(t *testing.T) { tx.Signers = []transaction.Signer{{ Account: testchain.CommitteeScriptHash(), Scopes: transaction.None, - }} + }, + { + Account: bc.contracts.Notary.Hash, + Scopes: transaction.None, + }, + } rawScript := testchain.CommitteeVerificationScript() - require.NoError(t, err) size := io.GetVarSize(tx) netFee, sizeDelta := fee.Calculate(rawScript) tx.NetworkFee += netFee tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() data := tx.GetSignedPart() - tx.Scripts = []transaction.Witness{{ - InvocationScript: testchain.SignCommittee(data), - VerificationScript: rawScript, - }} + tx.Scripts = []transaction.Witness{ + { + InvocationScript: testchain.SignCommittee(data), + VerificationScript: rawScript, + }, + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...), + }, + } return tx } t.Run("Disabled", func(t *testing.T) { bc.config.P2PSigExtensions = false tx := getNotaryAssistedTx(0, 0) - require.Error(t, bc.VerifyTx(tx)) + require.True(t, errors.Is(bc.VerifyTx(tx), ErrInvalidAttribute)) }) t.Run("Enabled, insufficient network fee", func(t *testing.T) { bc.config.P2PSigExtensions = true tx := getNotaryAssistedTx(1, 0) require.Error(t, bc.VerifyTx(tx)) }) - t.Run("Enabled, positive", func(t *testing.T) { + t.Run("Test verify", func(t *testing.T) { bc.config.P2PSigExtensions = true - tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey) - require.NoError(t, bc.VerifyTx(tx)) + t.Run("no NotaryAssisted attribute", func(t *testing.T) { + tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey) + tx.Attributes = []transaction.Attribute{} + tx.Signers = []transaction.Signer{ + { + Account: testchain.CommitteeScriptHash(), + Scopes: transaction.None, + }, + { + Account: bc.contracts.Notary.Hash, + Scopes: transaction.None, + }, + } + data := tx.GetSignedPart() + tx.Scripts = []transaction.Witness{ + { + InvocationScript: testchain.SignCommittee(data), + VerificationScript: testchain.CommitteeVerificationScript(), + }, + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("no deposit", func(t *testing.T) { + tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: bc.contracts.Notary.Hash, + Scopes: transaction.None, + }, + { + Account: testchain.CommitteeScriptHash(), + Scopes: transaction.None, + }, + } + data := tx.GetSignedPart() + tx.Scripts = []transaction.Witness{ + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...), + }, + { + InvocationScript: testchain.SignCommittee(data), + VerificationScript: testchain.CommitteeVerificationScript(), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("bad Notary signer scope", func(t *testing.T) { + tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: testchain.CommitteeScriptHash(), + Scopes: transaction.None, + }, + { + Account: bc.contracts.Notary.Hash, + Scopes: transaction.CalledByEntry, + }, + } + data := tx.GetSignedPart() + tx.Scripts = []transaction.Witness{ + { + InvocationScript: testchain.SignCommittee(data), + VerificationScript: testchain.CommitteeVerificationScript(), + }, + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("not signed by Notary", func(t *testing.T) { + tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: testchain.CommitteeScriptHash(), + Scopes: transaction.None, + }, + } + data := tx.GetSignedPart() + tx.Scripts = []transaction.Witness{ + { + InvocationScript: testchain.SignCommittee(data), + VerificationScript: testchain.CommitteeVerificationScript(), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("bad Notary node witness", func(t *testing.T) { + tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: testchain.CommitteeScriptHash(), + Scopes: transaction.None, + }, + { + Account: bc.contracts.Notary.Hash, + Scopes: transaction.None, + }, + } + data := tx.GetSignedPart() + acc, err := keys.NewPrivateKey() + require.NoError(t, err) + tx.Scripts = []transaction.Witness{ + { + InvocationScript: testchain.SignCommittee(data), + VerificationScript: testchain.CommitteeVerificationScript(), + }, + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc.Sign(data)...), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("missing payer", func(t *testing.T) { + tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey) + tx.Signers = []transaction.Signer{ + { + Account: bc.contracts.Notary.Hash, + Scopes: transaction.None, + }, + } + data := tx.GetSignedPart() + tx.Scripts = []transaction.Witness{ + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...), + }, + } + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("positive", func(t *testing.T) { + tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey) + require.NoError(t, bc.VerifyTx(tx)) + }) }) }) }) diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 50323e3e7..d9c854164 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -18,14 +18,18 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/chaindump" "github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" @@ -364,9 +368,9 @@ func initBasicChain(t *testing.T, bc *Blockchain) { require.NoError(t, bc.AddBlock(b)) } -func newNEP17Transfer(sc, from, to util.Uint160, amount int64) *transaction.Transaction { +func newNEP17Transfer(sc, from, to util.Uint160, amount int64, additionalArgs ...interface{}) *transaction.Transaction { w := io.NewBufBinWriter() - emit.AppCallWithOperationAndArgs(w.BinWriter, sc, "transfer", from, to, amount, nil) + emit.AppCallWithOperationAndArgs(w.BinWriter, sc, "transfer", from, to, amount, additionalArgs) emit.Opcodes(w.BinWriter, opcode.ASSERT) script := w.Bytes() @@ -411,3 +415,56 @@ func addNetworkFee(bc *Blockchain, tx *transaction.Transaction, sender *wallet.A tx.NetworkFee += int64(size) * bc.FeePerByte() return nil } + +func invokeContractMethod(chain *Blockchain, sysfee int64, hash util.Uint160, method string, args ...interface{}) (*state.AppExecResult, error) { + w := io.NewBufBinWriter() + emit.AppCallWithOperationAndArgs(w.BinWriter, hash, method, args...) + if w.Err != nil { + return nil, w.Err + } + script := w.Bytes() + tx := transaction.New(chain.GetConfig().Magic, script, sysfee) + tx.ValidUntilBlock = chain.blockHeight + 1 + addSigners(tx) + err := testchain.SignTx(chain, tx) + if err != nil { + return nil, err + } + b := chain.newBlock(tx) + err = chain.AddBlock(b) + if err != nil { + return nil, err + } + + res, err := chain.GetAppExecResults(tx.Hash(), trigger.Application) + if err != nil { + return nil, err + } + return &res[0], nil +} + +func transferTokenFromMultisigAccount(t *testing.T, chain *Blockchain, to, tokenHash util.Uint160, amount int64, additionalArgs ...interface{}) *transaction.Transaction { + transferTx := newNEP17Transfer(tokenHash, testchain.MultisigScriptHash(), to, amount, additionalArgs...) + transferTx.SystemFee = 100000000 + transferTx.ValidUntilBlock = chain.BlockHeight() + 1 + addSigners(transferTx) + require.NoError(t, testchain.SignTx(chain, transferTx)) + b := chain.newBlock(transferTx) + require.NoError(t, chain.AddBlock(b)) + return transferTx +} + +func checkResult(t *testing.T, result *state.AppExecResult, expected stackitem.Item) { + require.Equal(t, vm.HaltState, result.VMState) + require.Equal(t, 1, len(result.Stack)) + require.Equal(t, expected, result.Stack[0]) +} + +func checkFAULTState(t *testing.T, result *state.AppExecResult) { + require.Equal(t, vm.FaultState, result.VMState) +} + +func checkBalanceOf(t *testing.T, chain *Blockchain, addr util.Uint160, expected int) { + balance := chain.GetNEP17Balances(addr).Trackers[chain.contracts.GAS.ContractID] + require.Equal(t, int64(expected), balance.Balance.Int64()) +} diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index 007ff8a1e..29f7e58b7 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -12,6 +12,9 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm/opcode" ) +// reservedContractID represents the upper bound of the reserved IDs for native contracts. +const reservedContractID = -100 + // Contracts is a set of registered native contracts. type Contracts struct { NEO *NEO @@ -19,6 +22,7 @@ type Contracts struct { Policy *Policy Oracle *Oracle Designate *Designate + Notary *Notary Contracts []interop.Contract // persistScript is vm script which executes "onPersist" method of every native contract. persistScript []byte @@ -47,8 +51,8 @@ func (cs *Contracts) ByName(name string) interop.Contract { return nil } -// NewContracts returns new set of native contracts with new GAS, NEO and Policy -// contracts. +// NewContracts returns new set of native contracts with new GAS, NEO, Policy, Oracle, +// Designate and (optional) Notary contracts. func NewContracts(p2pSigExtensionsEnabled bool) *Contracts { cs := new(Contracts) @@ -78,6 +82,14 @@ func NewContracts(p2pSigExtensionsEnabled bool) *Contracts { cs.Oracle.Desig = desig cs.Contracts = append(cs.Contracts, desig) + if p2pSigExtensionsEnabled { + notary := newNotary() + notary.GAS = gas + notary.Desig = desig + cs.Notary = notary + cs.Contracts = append(cs.Contracts, notary) + } + return cs } @@ -114,7 +126,7 @@ func (cs *Contracts) GetPostPersistScript() []byte { md := cs.Contracts[i].Metadata() // Not every contract is persisted: // https://github.com/neo-project/neo/blob/master/src/neo/Ledger/Blockchain.cs#L103 - if md.ContractID == policyContractID || md.ContractID == gasContractID || md.ContractID == designateContractID { + if md.ContractID == policyContractID || md.ContractID == gasContractID || md.ContractID == designateContractID || md.ContractID == notaryContractID { continue } emit.Int(w.BinWriter, 0) diff --git a/pkg/core/native/native_nep17.go b/pkg/core/native/native_nep17.go index ddb24479a..c8ddc6a72 100644 --- a/pkg/core/native/native_nep17.go +++ b/pkg/core/native/native_nep17.go @@ -3,6 +3,7 @@ package native import ( "errors" "fmt" + "math" "math/big" "github.com/nspcc-dev/neo-go/pkg/core/dao" @@ -320,6 +321,18 @@ func toUint160(s stackitem.Item) util.Uint160 { return u } +func toUint32(s stackitem.Item) uint32 { + bigInt := toBigInt(s) + if !bigInt.IsInt64() { + panic("bigint is not an int64") + } + int64Value := bigInt.Int64() + if int64Value < 0 || int64Value > math.MaxUint32 { + panic("bigint does not fit into uint32") + } + return uint32(int64Value) +} + func getOnPersistWrapper(f func(ic *interop.Context) error) interop.Method { return func(ic *interop.Context, _ []stackitem.Item) stackitem.Item { err := f(ic) diff --git a/pkg/core/native/notary.go b/pkg/core/native/notary.go new file mode 100644 index 000000000..9282c5211 --- /dev/null +++ b/pkg/core/native/notary.go @@ -0,0 +1,357 @@ +package native + +import ( + "errors" + "fmt" + "math" + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/core/dao" + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/core/storage" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +// Notary represents Notary native contract. +type Notary struct { + interop.ContractMD + GAS *GAS + Desig *Designate +} + +const ( + notaryName = "Notary" + notaryContractID = reservedContractID - 1 + + // prefixDeposit is a prefix for storing Notary deposits. + prefixDeposit = 1 +) + +// newNotary returns Notary native contract. +func newNotary() *Notary { + n := &Notary{ContractMD: *interop.NewContractMD(notaryName)} + n.ContractID = notaryContractID + + desc := newDescriptor("onPayment", smartcontract.VoidType, + manifest.NewParameter("from", smartcontract.Hash160Type), + manifest.NewParameter("amount", smartcontract.IntegerType), + manifest.NewParameter("data", smartcontract.AnyType)) + md := newMethodAndPrice(n.onPayment, 100_0000, smartcontract.AllowModifyStates) + n.AddMethod(md, desc, true) + + desc = newDescriptor("lockDepositUntil", smartcontract.BoolType, + manifest.NewParameter("address", smartcontract.Hash160Type), + manifest.NewParameter("till", smartcontract.IntegerType)) + md = newMethodAndPrice(n.lockDepositUntil, 100_0000, smartcontract.AllowModifyStates) + n.AddMethod(md, desc, true) + + desc = newDescriptor("withdraw", smartcontract.BoolType, + manifest.NewParameter("from", smartcontract.Hash160Type), + manifest.NewParameter("to", smartcontract.Hash160Type)) + md = newMethodAndPrice(n.withdraw, 100_0000, smartcontract.AllowModifyStates) + n.AddMethod(md, desc, true) + + desc = newDescriptor("balanceOf", smartcontract.IntegerType, + manifest.NewParameter("addr", smartcontract.Hash160Type)) + md = newMethodAndPrice(n.balanceOf, 100_0000, smartcontract.AllowStates) + n.AddMethod(md, desc, true) + + desc = newDescriptor("expirationOf", smartcontract.IntegerType, + manifest.NewParameter("addr", smartcontract.Hash160Type)) + md = newMethodAndPrice(n.expirationOf, 100_0000, smartcontract.AllowStates) + n.AddMethod(md, desc, true) + + desc = newDescriptor("verify", smartcontract.BoolType, + manifest.NewParameter("signature", smartcontract.SignatureType)) + md = newMethodAndPrice(n.verify, 100_0000, smartcontract.AllowStates) + n.AddMethod(md, desc, false) + + desc = newDescriptor("onPersist", smartcontract.VoidType) + md = newMethodAndPrice(getOnPersistWrapper(n.OnPersist), 0, smartcontract.AllowModifyStates) + n.AddMethod(md, desc, false) + + desc = newDescriptor("postPersist", smartcontract.VoidType) + md = newMethodAndPrice(getOnPersistWrapper(postPersistBase), 0, smartcontract.AllowModifyStates) + n.AddMethod(md, desc, false) + + return n +} + +// Metadata implements Contract interface. +func (n *Notary) Metadata() *interop.ContractMD { + return &n.ContractMD +} + +// Initialize initializes Notary native contract and implements Contract interface. +func (n *Notary) Initialize(ic *interop.Context) error { + return nil +} + +// OnPersist implements Contract interface. +func (n *Notary) OnPersist(ic *interop.Context) error { + var ( + nFees int64 + notaries keys.PublicKeys + err error + ) + for _, tx := range ic.Block.Transactions { + if tx.HasAttribute(transaction.NotaryAssistedT) { + if notaries == nil { + notaries, err = n.GetNotaryNodes(ic.DAO) + if err != nil { + return fmt.Errorf("failed to get notary nodes: %w", err) + } + } + nKeys := tx.GetAttributes(transaction.NotaryAssistedT)[0].Value.(*transaction.NotaryAssisted).NKeys + nFees += int64(nKeys) + 1 + if tx.Sender() == n.Hash { + payer := tx.Signers[1] + balance := n.getDepositFor(ic.DAO, payer.Account) + balance.Amount.Sub(balance.Amount, big.NewInt(tx.SystemFee+tx.NetworkFee)) + if balance.Amount.Sign() == 0 { + err := n.removeDepositFor(ic.DAO, payer.Account) + if err != nil { + return fmt.Errorf("failed to remove an empty deposit for %s from storage: %w", payer.Account.StringBE(), err) + } + } else { + err := n.putDepositFor(ic.DAO, balance, payer.Account) + if err != nil { + return fmt.Errorf("failed to update deposit for %s: %w", payer.Account.StringBE(), err) + } + } + } + } + } + if nFees == 0 { + return nil + } + singleReward := calculateNotaryReward(nFees, len(notaries)) + for _, notary := range notaries { + n.GAS.mint(ic, notary.GetScriptHash(), singleReward) + } + return nil +} + +// onPayment records deposited amount as belonging to "from" address with a lock +// till the specified chain's height. +func (n *Notary) onPayment(ic *interop.Context, args []stackitem.Item) stackitem.Item { + if h := ic.VM.GetCallingScriptHash(); h != n.GAS.Hash { + panic(fmt.Errorf("only GAS can be accepted for deposit, got %s", h.StringBE())) + } + from := toUint160(args[0]) + to := from + amount := toBigInt(args[1]) + data, ok := args[2].(*stackitem.Array) + if !ok || len(data.Value().([]stackitem.Item)) != 2 { + panic(errors.New("`data` parameter should be an array of 2 elements")) + } + additionalParams := data.Value().([]stackitem.Item) + if !additionalParams[0].Equals(stackitem.Null{}) { + to = toUint160(additionalParams[0]) + } + till := toUint32(additionalParams[1]) + currentHeight := ic.Chain.BlockHeight() + if till < currentHeight { + panic(fmt.Errorf("`till` shouldn't be less then the chain's height %d", currentHeight)) + } + + deposit := n.getDepositFor(ic.DAO, to) + if deposit == nil { + if amount.Cmp(big.NewInt(2*transaction.NotaryServiceFeePerKey)) < 0 { + panic(fmt.Errorf("first deposit can not be less then %d, got %d", 2*transaction.NotaryServiceFeePerKey, amount.Int64())) + } + deposit = &state.Deposit{ + Amount: new(big.Int), + } + } else { + if till < deposit.Till { + panic(fmt.Errorf("`till` shouldn't be less then the previous value %d", deposit.Till)) + } + } + deposit.Amount.Add(deposit.Amount, amount) + deposit.Till = till + + if err := n.putDepositFor(ic.DAO, deposit, to); err != nil { + panic(fmt.Errorf("failed to put deposit for %s into the storage: %w", from.StringBE(), err)) + } + return stackitem.Null{} +} + +// lockDepositUntil updates the chain's height until which deposit is locked. +func (n *Notary) lockDepositUntil(ic *interop.Context, args []stackitem.Item) stackitem.Item { + addr := toUint160(args[0]) + ok, err := runtime.CheckHashedWitness(ic, addr) + if err != nil { + panic(fmt.Errorf("failed to check witness for %s: %w", addr.StringBE(), err)) + } + if !ok { + return stackitem.NewBool(false) + } + till := toUint32(args[1]) + if till < ic.Chain.BlockHeight() { + return stackitem.NewBool(false) + } + deposit := n.getDepositFor(ic.DAO, addr) + if deposit == nil { + return stackitem.NewBool(false) + } + if till < deposit.Till { + return stackitem.NewBool(false) + } + deposit.Till = till + err = n.putDepositFor(ic.DAO, deposit, addr) + if err != nil { + panic(fmt.Errorf("failed to put deposit for %s into the storage: %w", addr.StringBE(), err)) + } + return stackitem.NewBool(true) +} + +// withdraw sends all deposited GAS for "from" address to "to" address. +func (n *Notary) withdraw(ic *interop.Context, args []stackitem.Item) stackitem.Item { + from := toUint160(args[0]) + ok, err := runtime.CheckHashedWitness(ic, from) + if err != nil { + panic(fmt.Errorf("failed to check witness for %s: %w", from.StringBE(), err)) + } + if !ok { + return stackitem.NewBool(false) + } + to := from + if !args[1].Equals(stackitem.Null{}) { + to = toUint160(args[1]) + } + deposit := n.getDepositFor(ic.DAO, from) + if deposit == nil { + return stackitem.NewBool(false) + } + if ic.Chain.BlockHeight() < deposit.Till { + return stackitem.NewBool(false) + } + cs, err := ic.DAO.GetContractState(n.GAS.Hash) + if err != nil { + panic(fmt.Errorf("failed to get GAS contract state: %w", err)) + } + transferArgs := []stackitem.Item{stackitem.NewByteArray(n.Hash.BytesBE()), stackitem.NewByteArray(to.BytesBE()), stackitem.NewBigInteger(deposit.Amount), stackitem.Null{}} + err = contract.CallExInternal(ic, cs, "transfer", transferArgs, smartcontract.All, vm.EnsureIsEmpty, func(ctx *vm.Context) { // we need EnsureIsEmpty because there's a callback popping result from the stack + isTransferOk := ic.VM.Estack().Pop().Bool() + if !isTransferOk { + panic("failed to transfer GAS from Notary account") + } + }) + if err != nil { + panic(fmt.Errorf("failed to transfer GAS from Notary account: %w", err)) + } + if err := n.removeDepositFor(ic.DAO, from); err != nil { + panic(fmt.Errorf("failed to remove withdrawn deposit for %s from the storage: %w", from.StringBE(), err)) + } + return stackitem.NewBool(true) +} + +// balanceOf returns deposited GAS amount for specified address. +func (n *Notary) balanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item { + acc := toUint160(args[0]) + deposit := n.getDepositFor(ic.DAO, acc) + if deposit == nil { + return stackitem.NewBigInteger(big.NewInt(0)) + } + return stackitem.NewBigInteger(deposit.Amount) +} + +// expirationOf Returns deposit lock height for specified address. +func (n *Notary) expirationOf(ic *interop.Context, args []stackitem.Item) stackitem.Item { + acc := toUint160(args[0]) + deposit := n.getDepositFor(ic.DAO, acc) + if deposit == nil { + return stackitem.Make(0) + } + return stackitem.Make(deposit.Till) +} + +// verify checks whether the transaction was signed by one of the notaries. +func (n *Notary) verify(ic *interop.Context, args []stackitem.Item) stackitem.Item { + sig, err := args[0].TryBytes() + if err != nil { + panic(fmt.Errorf("failed to get signature bytes: %w", err)) + } + tx := ic.Tx + if len(tx.GetAttributes(transaction.NotaryAssistedT)) == 0 { + return stackitem.NewBool(false) + } + for _, signer := range tx.Signers { + if signer.Account == n.Hash { + if signer.Scopes != transaction.None { + return stackitem.NewBool(false) + } + } + } + if tx.Sender() == n.Hash { + if len(tx.Signers) != 2 { + return stackitem.NewBool(false) + } + payer := tx.Signers[1].Account + balance := n.getDepositFor(ic.DAO, payer) + if balance == nil || balance.Amount.Cmp(big.NewInt(tx.NetworkFee+tx.SystemFee)) < 0 { + return stackitem.NewBool(false) + } + } + notaries, err := n.GetNotaryNodes(ic.DAO) + if err != nil { + panic(fmt.Errorf("failed to get notary nodes: %w", err)) + } + hash := tx.GetSignedHash().BytesBE() + var verified bool + for _, n := range notaries { + if n.Verify(sig, hash) { + verified = true + break + } + } + return stackitem.NewBool(verified) +} + +// GetNotaryNodes returns public keys of notary nodes. +func (n *Notary) GetNotaryNodes(d dao.DAO) (keys.PublicKeys, error) { + nodes, _, err := n.Desig.GetDesignatedByRole(d, RoleP2PNotary, math.MaxUint32) + return nodes, err +} + +// getDepositFor returns state.Deposit for the account specified. It returns nil in case if +// deposit is not found in storage and panics in case of any other error. +func (n *Notary) getDepositFor(dao dao.DAO, acc util.Uint160) *state.Deposit { + key := append([]byte{prefixDeposit}, acc.BytesBE()...) + deposit := new(state.Deposit) + err := getSerializableFromDAO(n.ContractID, dao, key, deposit) + if err == nil { + return deposit + } + if err == storage.ErrKeyNotFound { + return nil + } + panic(fmt.Errorf("failed to get deposit for %s from storage: %w", acc.StringBE(), err)) +} + +// putDepositFor puts deposit on the balance of the specified account in the storage. +func (n *Notary) putDepositFor(dao dao.DAO, deposit *state.Deposit, acc util.Uint160) error { + key := append([]byte{prefixDeposit}, acc.BytesBE()...) + return putSerializableToDAO(n.ContractID, dao, key, deposit) +} + +// removeDepositFor removes deposit from the storage. +func (n *Notary) removeDepositFor(dao dao.DAO, acc util.Uint160) error { + key := append([]byte{prefixDeposit}, acc.BytesBE()...) + return dao.DeleteStorageItem(n.ContractID, key) +} + +// calculateNotaryReward calculates the reward for a single notary node based on FEE's count and Notary nodes count. +func calculateNotaryReward(nFees int64, notariesCount int) *big.Int { + return big.NewInt(nFees * transaction.NotaryServiceFeePerKey / int64(notariesCount)) +} diff --git a/pkg/core/native/util.go b/pkg/core/native/util.go index 293581c34..2ca752105 100644 --- a/pkg/core/native/util.go +++ b/pkg/core/native/util.go @@ -2,6 +2,7 @@ package native import ( "github.com/nspcc-dev/neo-go/pkg/core/dao" + "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/io" ) @@ -15,3 +16,14 @@ func getSerializableFromDAO(id int32, d dao.DAO, key []byte, item io.Serializabl item.DecodeBinary(r) return r.Err } + +func putSerializableToDAO(id int32, d dao.DAO, key []byte, item io.Serializable) error { + w := io.NewBufBinWriter() + item.EncodeBinary(w.BinWriter) + if w.Err != nil { + return w.Err + } + return d.PutStorageItem(id, key, &state.StorageItem{ + Value: w.Bytes(), + }) +} diff --git a/pkg/core/native_notary_test.go b/pkg/core/native_notary_test.go new file mode 100644 index 000000000..33d9703a6 --- /dev/null +++ b/pkg/core/native_notary_test.go @@ -0,0 +1,298 @@ +package core + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/internal/testchain" + "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/stretchr/testify/require" +) + +func TestNotaryContractPipeline(t *testing.T) { + chain := newTestChain(t) + defer chain.Close() + + notaryHash := chain.contracts.Notary.Hash + gasHash := chain.contracts.GAS.Hash + depositLock := 30 + + // check Notary contract has no GAS on the account + checkBalanceOf(t, chain, notaryHash, 0) + + // `balanceOf`: check multisig account has no GAS on deposit + balance, err := invokeContractMethod(chain, 100000000, notaryHash, "balanceOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, balance, stackitem.Make(0)) + + // `expirationOf`: should fail to get deposit which does not exist + till, err := invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(0)) + + // `lockDepositUntil`: should fail because there's no deposit + lockDepositUntilRes, err := invokeContractMethod(chain, 100000000, notaryHash, "lockDepositUntil", testchain.MultisigScriptHash(), int64(depositLock+1)) + require.NoError(t, err) + checkResult(t, lockDepositUntilRes, stackitem.NewBool(false)) + + // `onPayment`: bad token + transferTx := transferTokenFromMultisigAccount(t, chain, notaryHash, chain.contracts.NEO.Hash, 1, nil, int64(depositLock)) + res, err := chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + checkFAULTState(t, &res[0]) + + // `onPayment`: insufficient first deposit + transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 2*transaction.NotaryServiceFeePerKey-1, nil, int64(depositLock)) + res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + checkFAULTState(t, &res[0]) + + // `onPayment`: invalid `data` (missing `till` parameter) + transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 2*transaction.NotaryServiceFeePerKey-1, nil) + res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + checkFAULTState(t, &res[0]) + + // `onPayment`: good + transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 2*transaction.NotaryServiceFeePerKey, nil, int64(depositLock)) + res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + require.Equal(t, vm.HaltState, res[0].VMState) + require.Equal(t, 0, len(res[0].Stack)) + checkBalanceOf(t, chain, notaryHash, 2*transaction.NotaryServiceFeePerKey) + + // `expirationOf`: check `till` was set + till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(depositLock)) + + // `balanceOf`: check deposited amount for the multisig account + balance, err = invokeContractMethod(chain, 100000000, notaryHash, "balanceOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, balance, stackitem.Make(2*transaction.NotaryServiceFeePerKey)) + + // `onPayment`: good second deposit and explicit `to` paramenter + transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, transaction.NotaryServiceFeePerKey, testchain.MultisigScriptHash(), int64(depositLock+1)) + res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + require.Equal(t, vm.HaltState, res[0].VMState) + require.Equal(t, 0, len(res[0].Stack)) + checkBalanceOf(t, chain, notaryHash, 3*transaction.NotaryServiceFeePerKey) + + // `balanceOf`: check deposited amount for the multisig account + balance, err = invokeContractMethod(chain, 100000000, notaryHash, "balanceOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, balance, stackitem.Make(3*transaction.NotaryServiceFeePerKey)) + + // `expirationOf`: check `till` is updated. + till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(depositLock+1)) + + // `onPayment`: empty payment, should fail because `till` less then the previous one + transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 0, testchain.MultisigScriptHash(), int64(depositLock)) + res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + checkFAULTState(t, &res[0]) + checkBalanceOf(t, chain, notaryHash, 3*transaction.NotaryServiceFeePerKey) + till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(depositLock+1)) + + // `onPayment`: empty payment, should fail because `till` less then the chain height + transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 0, testchain.MultisigScriptHash(), int64(1)) + res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + checkFAULTState(t, &res[0]) + checkBalanceOf(t, chain, notaryHash, 3*transaction.NotaryServiceFeePerKey) + till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(depositLock+1)) + + // `onPayment`: empty payment, should successfully update `till` + transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 0, testchain.MultisigScriptHash(), int64(depositLock+2)) + res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + require.Equal(t, vm.HaltState, res[0].VMState) + require.Equal(t, 0, len(res[0].Stack)) + checkBalanceOf(t, chain, notaryHash, 3*transaction.NotaryServiceFeePerKey) + till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(depositLock+2)) + + // `lockDepositUntil`: bad witness + lockDepositUntilRes, err = invokeContractMethod(chain, 100000000, notaryHash, "lockDepositUntil", util.Uint160{1, 2, 3}, int64(depositLock+5)) + require.NoError(t, err) + checkResult(t, lockDepositUntilRes, stackitem.NewBool(false)) + till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(depositLock+2)) + + // `lockDepositUntil`: bad `till` (less then the previous one) + lockDepositUntilRes, err = invokeContractMethod(chain, 100000000, notaryHash, "lockDepositUntil", testchain.MultisigScriptHash(), int64(depositLock+1)) + require.NoError(t, err) + checkResult(t, lockDepositUntilRes, stackitem.NewBool(false)) + till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(depositLock+2)) + + // `lockDepositUntil`: bad `till` (less then the chain's height) + lockDepositUntilRes, err = invokeContractMethod(chain, 100000000, notaryHash, "lockDepositUntil", testchain.MultisigScriptHash(), int64(1)) + require.NoError(t, err) + checkResult(t, lockDepositUntilRes, stackitem.NewBool(false)) + till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(depositLock+2)) + + // `lockDepositUntil`: good `till` + lockDepositUntilRes, err = invokeContractMethod(chain, 100000000, notaryHash, "lockDepositUntil", testchain.MultisigScriptHash(), int64(depositLock+3)) + require.NoError(t, err) + checkResult(t, lockDepositUntilRes, stackitem.NewBool(true)) + till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, till, stackitem.Make(depositLock+3)) + + // transfer 1 GAS to the new account for the next test + acc, _ := wallet.NewAccount() + transferTx = transferTokenFromMultisigAccount(t, chain, acc.PrivateKey().PublicKey().GetScriptHash(), gasHash, 100000000) + res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + require.Equal(t, vm.HaltState, res[0].VMState) + require.Equal(t, 0, len(res[0].Stack)) + + // `withdraw`: bad witness + w := io.NewBufBinWriter() + emit.AppCallWithOperationAndArgs(w.BinWriter, notaryHash, "withdraw", testchain.MultisigScriptHash(), acc.PrivateKey().PublicKey().GetScriptHash()) + require.NoError(t, w.Err) + script := w.Bytes() + withdrawTx := transaction.New(chain.GetConfig().Magic, script, 10000000) + withdrawTx.ValidUntilBlock = chain.blockHeight + 1 + withdrawTx.NetworkFee = 10000000 + withdrawTx.Signers = []transaction.Signer{ + { + Account: acc.PrivateKey().PublicKey().GetScriptHash(), + Scopes: transaction.None, + }, + } + err = acc.SignTx(withdrawTx) + require.NoError(t, err) + b := chain.newBlock(withdrawTx) + err = chain.AddBlock(b) + require.NoError(t, err) + appExecRes, err := chain.GetAppExecResults(withdrawTx.Hash(), trigger.Application) + require.NoError(t, err) + checkResult(t, &appExecRes[0], stackitem.NewBool(false)) + balance, err = invokeContractMethod(chain, 100000000, notaryHash, "balanceOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, balance, stackitem.Make(3*transaction.NotaryServiceFeePerKey)) + + // `withdraw`: locked deposit + withdrawRes, err := invokeContractMethod(chain, 100000000, notaryHash, "withdraw", testchain.MultisigScriptHash(), acc.PrivateKey().PublicKey().GetScriptHash()) + require.NoError(t, err) + checkResult(t, withdrawRes, stackitem.NewBool(false)) + balance, err = invokeContractMethod(chain, 100000000, notaryHash, "balanceOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, balance, stackitem.Make(3*transaction.NotaryServiceFeePerKey)) + + // `withdraw`: unlock deposit and transfer GAS back to owner + chain.genBlocks(depositLock) + withdrawRes, err = invokeContractMethod(chain, 100000000, notaryHash, "withdraw", testchain.MultisigScriptHash(), testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, withdrawRes, stackitem.NewBool(true)) + balance, err = invokeContractMethod(chain, 100000000, notaryHash, "balanceOf", testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, balance, stackitem.Make(0)) + checkBalanceOf(t, chain, notaryHash, 0) + + // `withdraw`: the second time it should fail, because there's no deposit left + withdrawRes, err = invokeContractMethod(chain, 100000000, notaryHash, "withdraw", testchain.MultisigScriptHash(), testchain.MultisigScriptHash()) + require.NoError(t, err) + checkResult(t, withdrawRes, stackitem.NewBool(false)) +} + +func TestNotaryNodesReward(t *testing.T) { + checkReward := func(nKeys int, nNotaryNodes int, spendFullDeposit bool) { + chain := newTestChain(t) + defer chain.Close() + notaryHash := chain.contracts.Notary.Hash + gasHash := chain.contracts.GAS.Hash + signer := testchain.MultisigScriptHash() + var err error + + // set Notary nodes and check their balance + notaryNodes := make([]*keys.PrivateKey, nNotaryNodes) + notaryNodesPublicKeys := make(keys.PublicKeys, nNotaryNodes) + for i := range notaryNodes { + notaryNodes[i], err = keys.NewPrivateKey() + require.NoError(t, err) + notaryNodesPublicKeys[i] = notaryNodes[i].PublicKey() + } + chain.setNodesByRole(t, true, native.RoleP2PNotary, notaryNodesPublicKeys) + for _, notaryNode := range notaryNodesPublicKeys { + checkBalanceOf(t, chain, notaryNode.GetScriptHash(), 0) + } + + // deposit GAS for `signer` with lock until the next block + depositAmount := 100_0000 + (2+int64(nKeys))*transaction.NotaryServiceFeePerKey // sysfee + netfee of the next transaction + if !spendFullDeposit { + depositAmount += 1_0000 + } + transferTx := transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, depositAmount, signer, int64(chain.BlockHeight()+1)) + res, err := chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + require.Equal(t, vm.HaltState, res[0].VMState) + require.Equal(t, 0, len(res[0].Stack)) + + // send transaction with Notary contract as a sender + tx := chain.newTestTx(util.Uint160{}, []byte{byte(opcode.PUSH1)}) + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: uint8(nKeys)}}) + tx.NetworkFee = (2 + int64(nKeys)) * transaction.NotaryServiceFeePerKey + tx.Signers = []transaction.Signer{ + { + Account: notaryHash, + Scopes: transaction.None, + }, + { + Account: signer, + Scopes: transaction.None, + }, + } + data := tx.GetSignedPart() + tx.Scripts = []transaction.Witness{ + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notaryNodes[0].Sign(data)...), + }, + { + InvocationScript: testchain.Sign(data), + VerificationScript: testchain.MultisigVerificationScript(), + }, + } + b := chain.newBlock(tx) + require.NoError(t, chain.AddBlock(b)) + checkBalanceOf(t, chain, notaryHash, int(depositAmount-tx.SystemFee-tx.NetworkFee)) + for _, notaryNode := range notaryNodesPublicKeys { + checkBalanceOf(t, chain, notaryNode.GetScriptHash(), transaction.NotaryServiceFeePerKey*(nKeys+1)/nNotaryNodes) + } + } + + for _, spendDeposit := range []bool{true, false} { + checkReward(0, 1, spendDeposit) + checkReward(0, 2, spendDeposit) + checkReward(1, 1, spendDeposit) + checkReward(1, 2, spendDeposit) + checkReward(1, 3, spendDeposit) + checkReward(5, 1, spendDeposit) + checkReward(5, 2, spendDeposit) + checkReward(5, 6, spendDeposit) + checkReward(5, 7, spendDeposit) + } +} diff --git a/pkg/core/native_policy_test.go b/pkg/core/native_policy_test.go index 496c6b1b5..b1fb38eca 100644 --- a/pkg/core/native_policy_test.go +++ b/pkg/core/native_policy_test.go @@ -5,18 +5,11 @@ import ( "testing" "github.com/nspcc-dev/neo-go/internal/random" - "github.com/nspcc-dev/neo-go/internal/testchain" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/native" - "github.com/nspcc-dev/neo-go/pkg/core/state" - "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" - "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/network/payload" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" - "github.com/nspcc-dev/neo-go/pkg/vm" - "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) @@ -24,6 +17,7 @@ import ( func TestMaxTransactionsPerBlock(t *testing.T) { chain := newTestChain(t) defer chain.Close() + policyHash := chain.contracts.Policy.Metadata().Hash t.Run("get, internal method", func(t *testing.T) { n := chain.contracts.Policy.GetMaxTransactionsPerBlockInternal(chain.dao) @@ -31,14 +25,14 @@ func TestMaxTransactionsPerBlock(t *testing.T) { }) t.Run("get, contract method", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "getMaxTransactionsPerBlock") + res, err := invokeContractMethod(chain, 100000000, policyHash, "getMaxTransactionsPerBlock") require.NoError(t, err) checkResult(t, res, stackitem.NewBigInteger(big.NewInt(512))) require.NoError(t, chain.persist()) }) t.Run("set", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "setMaxTransactionsPerBlock", bigint.ToBytes(big.NewInt(1024))) + res, err := invokeContractMethod(chain, 100000000, policyHash, "setMaxTransactionsPerBlock", bigint.ToBytes(big.NewInt(1024))) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) require.NoError(t, chain.persist()) @@ -47,7 +41,7 @@ func TestMaxTransactionsPerBlock(t *testing.T) { }) t.Run("set, too big value", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "setMaxTransactionsPerBlock", bigint.ToBytes(big.NewInt(block.MaxContentsPerBlock))) + res, err := invokeContractMethod(chain, 100000000, policyHash, "setMaxTransactionsPerBlock", bigint.ToBytes(big.NewInt(block.MaxContentsPerBlock))) require.NoError(t, err) checkFAULTState(t, res) }) @@ -56,6 +50,7 @@ func TestMaxTransactionsPerBlock(t *testing.T) { func TestMaxBlockSize(t *testing.T) { chain := newTestChain(t) defer chain.Close() + policyHash := chain.contracts.Policy.Metadata().Hash t.Run("get, internal method", func(t *testing.T) { n := chain.contracts.Policy.GetMaxBlockSizeInternal(chain.dao) @@ -63,25 +58,25 @@ func TestMaxBlockSize(t *testing.T) { }) t.Run("get, contract method", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "getMaxBlockSize") + res, err := invokeContractMethod(chain, 100000000, policyHash, "getMaxBlockSize") require.NoError(t, err) checkResult(t, res, stackitem.NewBigInteger(big.NewInt(1024*256))) require.NoError(t, chain.persist()) }) t.Run("set", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "setMaxBlockSize", bigint.ToBytes(big.NewInt(102400))) + res, err := invokeContractMethod(chain, 100000000, policyHash, "setMaxBlockSize", bigint.ToBytes(big.NewInt(102400))) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) require.NoError(t, chain.persist()) - res, err = invokeNativePolicyMethod(chain, "getMaxBlockSize") + res, err = invokeContractMethod(chain, 100000000, policyHash, "getMaxBlockSize") require.NoError(t, err) checkResult(t, res, stackitem.NewBigInteger(big.NewInt(102400))) require.NoError(t, chain.persist()) }) t.Run("set, too big value", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "setMaxBlockSize", bigint.ToBytes(big.NewInt(payload.MaxSize+1))) + res, err := invokeContractMethod(chain, 100000000, policyHash, "setMaxBlockSize", bigint.ToBytes(big.NewInt(payload.MaxSize+1))) require.NoError(t, err) checkFAULTState(t, res) }) @@ -90,6 +85,7 @@ func TestMaxBlockSize(t *testing.T) { func TestFeePerByte(t *testing.T) { chain := newTestChain(t) defer chain.Close() + policyHash := chain.contracts.Policy.Metadata().Hash t.Run("get, internal method", func(t *testing.T) { n := chain.contracts.Policy.GetFeePerByteInternal(chain.dao) @@ -97,14 +93,14 @@ func TestFeePerByte(t *testing.T) { }) t.Run("get, contract method", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "getFeePerByte") + res, err := invokeContractMethod(chain, 100000000, policyHash, "getFeePerByte") require.NoError(t, err) checkResult(t, res, stackitem.NewBigInteger(big.NewInt(1000))) require.NoError(t, chain.persist()) }) t.Run("set", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "setFeePerByte", bigint.ToBytes(big.NewInt(1024))) + res, err := invokeContractMethod(chain, 100000000, policyHash, "setFeePerByte", bigint.ToBytes(big.NewInt(1024))) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) require.NoError(t, chain.persist()) @@ -113,13 +109,13 @@ func TestFeePerByte(t *testing.T) { }) t.Run("set, negative value", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "setFeePerByte", bigint.ToBytes(big.NewInt(-1))) + res, err := invokeContractMethod(chain, 100000000, policyHash, "setFeePerByte", bigint.ToBytes(big.NewInt(-1))) require.NoError(t, err) checkFAULTState(t, res) }) t.Run("set, too big value", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "setFeePerByte", bigint.ToBytes(big.NewInt(100_000_000+1))) + res, err := invokeContractMethod(chain, 100000000, policyHash, "setFeePerByte", bigint.ToBytes(big.NewInt(100_000_000+1))) require.NoError(t, err) checkFAULTState(t, res) }) @@ -128,6 +124,7 @@ func TestFeePerByte(t *testing.T) { func TestBlockSystemFee(t *testing.T) { chain := newTestChain(t) defer chain.Close() + policyHash := chain.contracts.Policy.Metadata().Hash t.Run("get, internal method", func(t *testing.T) { n := chain.contracts.Policy.GetMaxBlockSystemFeeInternal(chain.dao) @@ -135,24 +132,24 @@ func TestBlockSystemFee(t *testing.T) { }) t.Run("get", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "getMaxBlockSystemFee") + res, err := invokeContractMethod(chain, 100000000, policyHash, "getMaxBlockSystemFee") require.NoError(t, err) checkResult(t, res, stackitem.NewBigInteger(big.NewInt(9000*native.GASFactor))) require.NoError(t, chain.persist()) }) t.Run("set, too low fee", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "setMaxBlockSystemFee", bigint.ToBytes(big.NewInt(4007600))) + res, err := invokeContractMethod(chain, 100000000, policyHash, "setMaxBlockSystemFee", bigint.ToBytes(big.NewInt(4007600))) require.NoError(t, err) checkFAULTState(t, res) }) t.Run("set, success", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "setMaxBlockSystemFee", bigint.ToBytes(big.NewInt(100000000))) + res, err := invokeContractMethod(chain, 100000000, policyHash, "setMaxBlockSystemFee", bigint.ToBytes(big.NewInt(100000000))) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) require.NoError(t, chain.persist()) - res, err = invokeNativePolicyMethod(chain, "getMaxBlockSystemFee") + res, err = invokeContractMethod(chain, 100000000, policyHash, "getMaxBlockSystemFee") require.NoError(t, err) checkResult(t, res, stackitem.NewBigInteger(big.NewInt(100000000))) require.NoError(t, chain.persist()) @@ -163,6 +160,7 @@ func TestBlockedAccounts(t *testing.T) { chain := newTestChain(t) defer chain.Close() account := util.Uint160{1, 2, 3} + policyHash := chain.contracts.Policy.Metadata().Hash t.Run("isBlocked, internal method", func(t *testing.T) { isBlocked := chain.contracts.Policy.IsBlockedInternal(chain.dao, random.Uint160()) @@ -170,14 +168,14 @@ func TestBlockedAccounts(t *testing.T) { }) t.Run("isBlocked, contract method", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "isBlocked", random.Uint160()) + res, err := invokeContractMethod(chain, 100000000, policyHash, "isBlocked", random.Uint160()) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(false)) require.NoError(t, chain.persist()) }) t.Run("block-unblock account", func(t *testing.T) { - res, err := invokeNativePolicyMethod(chain, "blockAccount", account.BytesBE()) + res, err := invokeContractMethod(chain, 100000000, policyHash, "blockAccount", account.BytesBE()) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) @@ -185,7 +183,7 @@ func TestBlockedAccounts(t *testing.T) { require.Equal(t, isBlocked, true) require.NoError(t, chain.persist()) - res, err = invokeNativePolicyMethod(chain, "unblockAccount", account.BytesBE()) + res, err = invokeContractMethod(chain, 100000000, policyHash, "unblockAccount", account.BytesBE()) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) @@ -196,65 +194,27 @@ func TestBlockedAccounts(t *testing.T) { t.Run("double-block", func(t *testing.T) { // block - res, err := invokeNativePolicyMethod(chain, "blockAccount", account.BytesBE()) + res, err := invokeContractMethod(chain, 100000000, policyHash, "blockAccount", account.BytesBE()) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) require.NoError(t, chain.persist()) // double-block should fail - res, err = invokeNativePolicyMethod(chain, "blockAccount", account.BytesBE()) + res, err = invokeContractMethod(chain, 100000000, policyHash, "blockAccount", account.BytesBE()) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(false)) require.NoError(t, chain.persist()) // unblock - res, err = invokeNativePolicyMethod(chain, "unblockAccount", account.BytesBE()) + res, err = invokeContractMethod(chain, 100000000, policyHash, "unblockAccount", account.BytesBE()) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(true)) require.NoError(t, chain.persist()) // unblock the same account should fail as we don't have it blocked - res, err = invokeNativePolicyMethod(chain, "unblockAccount", account.BytesBE()) + res, err = invokeContractMethod(chain, 100000000, policyHash, "unblockAccount", account.BytesBE()) require.NoError(t, err) checkResult(t, res, stackitem.NewBool(false)) require.NoError(t, chain.persist()) }) } - -func invokeNativePolicyMethod(chain *Blockchain, method string, args ...interface{}) (*state.AppExecResult, error) { - w := io.NewBufBinWriter() - emit.AppCallWithOperationAndArgs(w.BinWriter, chain.contracts.Policy.Metadata().Hash, method, args...) - if w.Err != nil { - return nil, w.Err - } - script := w.Bytes() - tx := transaction.New(chain.GetConfig().Magic, script, 10000000) - validUntil := chain.blockHeight + 1 - tx.ValidUntilBlock = validUntil - addSigners(tx) - err := testchain.SignTx(chain, tx) - if err != nil { - return nil, err - } - b := chain.newBlock(tx) - err = chain.AddBlock(b) - if err != nil { - return nil, err - } - - res, err := chain.GetAppExecResults(tx.Hash(), trigger.Application) - if err != nil { - return nil, err - } - return &res[0], nil -} - -func checkResult(t *testing.T, result *state.AppExecResult, expected stackitem.Item) { - require.Equal(t, vm.HaltState, result.VMState) - require.Equal(t, 1, len(result.Stack)) - require.Equal(t, expected, result.Stack[0]) -} - -func checkFAULTState(t *testing.T, result *state.AppExecResult) { - require.Equal(t, vm.FaultState, result.VMState) -} diff --git a/pkg/core/state/deposit.go b/pkg/core/state/deposit.go new file mode 100644 index 000000000..8e45e9595 --- /dev/null +++ b/pkg/core/state/deposit.go @@ -0,0 +1,26 @@ +package state + +import ( + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" + "github.com/nspcc-dev/neo-go/pkg/io" +) + +// Deposit represents GAS deposit from Notary contract. +type Deposit struct { + Amount *big.Int + Till uint32 +} + +// EncodeBinary implements io.Serializable interface. +func (d *Deposit) EncodeBinary(w *io.BinWriter) { + w.WriteVarBytes(bigint.ToBytes(d.Amount)) + w.WriteU32LE(d.Till) +} + +// DecodeBinary implements io.Serializable interface. +func (d *Deposit) DecodeBinary(r *io.BinReader) { + d.Amount = bigint.FromBytes(r.ReadVarBytes()) + d.Till = r.ReadU32LE() +} From 9faa63453cf8e2c55c4cc73c94c1ca4cd80e239d Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 25 Nov 2020 18:10:14 +0300 Subject: [PATCH 14/14] core: refactore some tests They have a lot of common code. --- pkg/core/native_contract_test.go | 63 ++++++------------------------- pkg/core/native_designate_test.go | 27 +++---------- pkg/core/native_neo_test.go | 9 +---- pkg/core/native_oracle_test.go | 15 ++------ 4 files changed, 20 insertions(+), 94 deletions(-) diff --git a/pkg/core/native_contract_test.go b/pkg/core/native_contract_test.go index a2b2ab497..221dd35a7 100644 --- a/pkg/core/native_contract_test.go +++ b/pkg/core/native_contract_test.go @@ -4,7 +4,6 @@ import ( "math/big" "testing" - "github.com/nspcc-dev/neo-go/internal/testchain" "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" @@ -12,14 +11,11 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/storage" - "github.com/nspcc-dev/neo-go/pkg/core/transaction" - "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" - "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) @@ -157,44 +153,24 @@ func TestNativeContract_Invoke(t *testing.T) { }) require.NoError(t, err) - w := io.NewBufBinWriter() - emit.AppCallWithOperationAndArgs(w.BinWriter, tn.Metadata().Hash, "sum", int64(14), int64(28)) - script := w.Bytes() // System.Contract.Call + "sum" itself + opcodes for pushing arguments (PACK is 15000) - tx := transaction.New(chain.GetConfig().Magic, script, testSumPrice*2+18000) - validUntil := chain.blockHeight + 1 - tx.ValidUntilBlock = validUntil - addSigners(tx) - require.NoError(t, testchain.SignTx(chain, tx)) - - // Enough for Call and other opcodes, but not enough for "sum" call. - tx2 := transaction.New(chain.GetConfig().Magic, script, testSumPrice*2+8000) - tx2.ValidUntilBlock = chain.blockHeight + 1 - addSigners(tx2) - require.NoError(t, testchain.SignTx(chain, tx2)) - - b := chain.newBlock(tx, tx2) - require.NoError(t, chain.AddBlock(b)) - - res, err := chain.GetAppExecResults(tx.Hash(), trigger.Application) + res, err := invokeContractMethod(chain, testSumPrice*2+18000, tn.Metadata().Hash, "sum", int64(14), int64(28)) require.NoError(t, err) - require.Equal(t, 1, len(res)) - require.Equal(t, vm.HaltState, res[0].VMState) - require.Equal(t, 1, len(res[0].Stack)) - require.Equal(t, big.NewInt(42), res[0].Stack[0].Value()) - - res, err = chain.GetAppExecResults(tx2.Hash(), trigger.Application) - require.NoError(t, err) - require.Equal(t, 1, len(res)) - require.Equal(t, vm.FaultState, res[0].VMState) - + checkResult(t, res, stackitem.Make(42)) require.NoError(t, chain.persist()) + select { case index := <-tn.blocks: require.Equal(t, chain.blockHeight, index) default: require.Fail(t, "onPersist wasn't called") } + + // Enough for Call and other opcodes, but not enough for "sum" call. + res, err = invokeContractMethod(chain, testSumPrice*2+8000, tn.Metadata().Hash, "sum", int64(14), int64(28)) + require.NoError(t, err) + checkFAULTState(t, res) + } func TestNativeContract_InvokeInternal(t *testing.T) { @@ -254,25 +230,8 @@ func TestNativeContract_InvokeOtherContract(t *testing.T) { require.NoError(t, chain.dao.PutContractState(cs)) t.Run("non-native, no return", func(t *testing.T) { - w := io.NewBufBinWriter() - emit.AppCallWithOperationAndArgs(w.BinWriter, tn.Metadata().Hash, "callOtherContractNoReturn", - cs.ScriptHash(), "justReturn", []interface{}{}) - require.NoError(t, w.Err) - script := w.Bytes() - tx := transaction.New(chain.GetConfig().Magic, script, testSumPrice*4+10000) - validUntil := chain.blockHeight + 1 - tx.ValidUntilBlock = validUntil - addSigners(tx) - require.NoError(t, testchain.SignTx(chain, tx)) - - b := chain.newBlock(tx) - require.NoError(t, chain.AddBlock(b)) - - res, err := chain.GetAppExecResults(tx.Hash(), trigger.Application) + res, err := invokeContractMethod(chain, testSumPrice*4+10000, tn.Metadata().Hash, "callOtherContractNoReturn", cs.ScriptHash(), "justReturn", []interface{}{}) require.NoError(t, err) - require.Equal(t, 1, len(res)) - require.Equal(t, vm.HaltState, res[0].VMState) - require.Equal(t, 1, len(res[0].Stack)) - require.Equal(t, stackitem.Null{}, res[0].Stack[0]) // simple call is done with EnsureNotEmpty + checkResult(t, res, stackitem.Null{}) // simple call is done with EnsureNotEmpty }) } diff --git a/pkg/core/native_designate_test.go b/pkg/core/native_designate_test.go index 697821f00..d318c8772 100644 --- a/pkg/core/native_designate_test.go +++ b/pkg/core/native_designate_test.go @@ -64,34 +64,17 @@ func (bc *Blockchain) setNodesByRole(t *testing.T, ok bool, r native.Role, nodes } func (bc *Blockchain) getNodesByRole(t *testing.T, ok bool, r native.Role, index uint32, resLen int) { - w := io.NewBufBinWriter() - emit.AppCallWithOperationAndArgs(w.BinWriter, bc.contracts.Designate.Hash, "getDesignatedByRole", int64(r), int64(index)) - require.NoError(t, w.Err) - tx := transaction.New(netmode.UnitTestNet, w.Bytes(), 0) - tx.NetworkFee = 10_000_000 - tx.SystemFee = 10_000_000 - tx.ValidUntilBlock = 100 - tx.Signers = []transaction.Signer{ - { - Account: testchain.MultisigScriptHash(), - Scopes: transaction.None, - }, - } - require.NoError(t, testchain.SignTx(bc, tx)) - require.NoError(t, bc.AddBlock(bc.newBlock(tx))) - - aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application) + res, err := invokeContractMethod(bc, 10_000_000, bc.contracts.Designate.Hash, "getDesignatedByRole", int64(r), int64(index)) require.NoError(t, err) - require.Equal(t, 1, len(aer)) if ok { - require.Equal(t, vm.HaltState, aer[0].VMState) - require.Equal(t, 1, len(aer[0].Stack)) - arrItem := aer[0].Stack[0] + require.Equal(t, vm.HaltState, res.VMState) + require.Equal(t, 1, len(res.Stack)) + arrItem := res.Stack[0] require.Equal(t, stackitem.ArrayT, arrItem.Type()) arr := arrItem.(*stackitem.Array) require.Equal(t, resLen, arr.Len()) } else { - require.Equal(t, vm.FaultState, aer[0].VMState) + checkFAULTState(t, res) } } diff --git a/pkg/core/native_neo_test.go b/pkg/core/native_neo_test.go index de83f2fc5..6dae37cd4 100644 --- a/pkg/core/native_neo_test.go +++ b/pkg/core/native_neo_test.go @@ -313,14 +313,7 @@ func TestNEO_TransferOnPayment(t *testing.T) { require.NoError(t, bc.dao.PutContractState(cs)) const amount = 2 - tx := newNEP17Transfer(bc.contracts.NEO.Hash, neoOwner, cs.ScriptHash(), amount) - tx.SystemFee += 1_000_000 - tx.NetworkFee = 10_000_000 - tx.ValidUntilBlock = bc.BlockHeight() + 1 - addSigners(tx) - require.NoError(t, testchain.SignTx(bc, tx)) - require.NoError(t, bc.AddBlock(bc.newBlock(tx))) - + tx := transferTokenFromMultisigAccount(t, bc, cs.ScriptHash(), bc.contracts.NEO.Hash, amount) aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application) require.NoError(t, err) require.Equal(t, vm.HaltState, aer[0].VMState) diff --git a/pkg/core/native_oracle_test.go b/pkg/core/native_oracle_test.go index 7f0612044..9d9a310f7 100644 --- a/pkg/core/native_oracle_test.go +++ b/pkg/core/native_oracle_test.go @@ -90,23 +90,14 @@ func getOracleContractState(h util.Uint160) *state.Contract { func putOracleRequest(t *testing.T, h util.Uint160, bc *Blockchain, url string, filter *string, userData []byte, gas int64) util.Uint256 { - w := io.NewBufBinWriter() var filtItem interface{} if filter != nil { filtItem = *filter } - emit.AppCallWithOperationAndArgs(w.BinWriter, h, "requestURL", + res, err := invokeContractMethod(bc, gas+50_000_000+5_000_000, h, "requestURL", url, filtItem, "handle", userData, gas) - require.NoError(t, w.Err) - - gas += 50_000_000 + 5_000_000 // request + contract call with args - tx := transaction.New(netmode.UnitTestNet, w.Bytes(), gas) - tx.ValidUntilBlock = bc.BlockHeight() + 1 - tx.NetworkFee = 1_000_000 - setSigner(tx, testchain.MultisigScriptHash()) - require.NoError(t, testchain.SignTx(bc, tx)) - require.NoError(t, bc.AddBlock(bc.newBlock(tx))) - return tx.Hash() + require.NoError(t, err) + return res.Container } func TestOracle_Request(t *testing.T) {