github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/core/native/native_test/notary_test.go (about)

     1  package native_test
     2  
     3  import (
     4  	"math"
     5  	"math/big"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/nspcc-dev/neo-go/internal/random"
    10  	"github.com/nspcc-dev/neo-go/pkg/compiler"
    11  	"github.com/nspcc-dev/neo-go/pkg/config"
    12  	"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
    13  	"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
    14  	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
    15  	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
    16  	"github.com/nspcc-dev/neo-go/pkg/neotest"
    17  	"github.com/nspcc-dev/neo-go/pkg/neotest/chain"
    18  	"github.com/nspcc-dev/neo-go/pkg/rpcclient/notary"
    19  	"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
    20  	"github.com/nspcc-dev/neo-go/pkg/util"
    21  	"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
    22  	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
    23  	"github.com/stretchr/testify/require"
    24  )
    25  
    26  func newNotaryClient(t *testing.T) *neotest.ContractInvoker {
    27  	bc, acc := chain.NewSingleWithCustomConfig(t, func(cfg *config.Blockchain) {
    28  		cfg.P2PSigExtensions = true
    29  	})
    30  	e := neotest.NewExecutor(t, bc, acc, acc)
    31  
    32  	return e.CommitteeInvoker(e.NativeHash(t, nativenames.Notary))
    33  }
    34  
    35  func TestNotary_MaxNotValidBeforeDelta(t *testing.T) {
    36  	c := newNotaryClient(t)
    37  	testGetSet(t, c, "MaxNotValidBeforeDelta", 140, int64(c.Chain.GetConfig().ValidatorsCount), int64(c.Chain.GetConfig().MaxValidUntilBlockIncrement/2))
    38  }
    39  
    40  func TestNotary_MaxNotValidBeforeDeltaCache(t *testing.T) {
    41  	c := newNotaryClient(t)
    42  	testGetSetCache(t, c, "MaxNotValidBeforeDelta", 140)
    43  }
    44  
    45  func TestNotary_Pipeline(t *testing.T) {
    46  	notaryCommitteeInvoker := newNotaryClient(t)
    47  	e := notaryCommitteeInvoker.Executor
    48  	neoCommitteeInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Neo))
    49  	gasCommitteeInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Gas))
    50  
    51  	notaryHash := notaryCommitteeInvoker.NativeHash(t, nativenames.Notary)
    52  	feePerKey := e.Chain.GetNotaryServiceFeePerKey()
    53  	multisigHash := notaryCommitteeInvoker.Validator.ScriptHash() // matches committee's one for single chain
    54  	depositLock := 100
    55  
    56  	checkBalanceOf := func(t *testing.T, acc util.Uint160, expected int64) { // we don't have big numbers in this test, thus may use int
    57  		notaryCommitteeInvoker.CheckGASBalance(t, acc, big.NewInt(expected))
    58  	}
    59  
    60  	// check Notary contract has no GAS on the account
    61  	checkBalanceOf(t, notaryHash, 0)
    62  
    63  	// `balanceOf`: check multisig account has no GAS on deposit
    64  	notaryCommitteeInvoker.Invoke(t, 0, "balanceOf", multisigHash)
    65  
    66  	// `expirationOf`: should fail to get deposit which does not exist
    67  	notaryCommitteeInvoker.Invoke(t, 0, "expirationOf", multisigHash)
    68  
    69  	// `lockDepositUntil`: should fail because there's no deposit
    70  	notaryCommitteeInvoker.Invoke(t, false, "lockDepositUntil", multisigHash, int64(depositLock+1))
    71  
    72  	// `onPayment`: bad token
    73  	neoCommitteeInvoker.InvokeFail(t, "only GAS can be accepted for deposit", "transfer", multisigHash, notaryHash, int64(1), &notary.OnNEP17PaymentData{Till: uint32(depositLock)})
    74  
    75  	// `onPayment`: insufficient first deposit
    76  	gasCommitteeInvoker.InvokeFail(t, "first deposit can not be less than", "transfer", multisigHash, notaryHash, int64(2*feePerKey-1), &notary.OnNEP17PaymentData{Till: uint32(depositLock)})
    77  
    78  	// `onPayment`: invalid `data` (missing `till` parameter)
    79  	gasCommitteeInvoker.InvokeFail(t, "`data` parameter should be an array of 2 elements", "transfer", multisigHash, notaryHash, 2*feePerKey, []any{nil})
    80  
    81  	// `onPayment`: invalid `data` (outdated `till` parameter)
    82  	gasCommitteeInvoker.InvokeFail(t, "`till` shouldn't be less than the chain's height", "transfer", multisigHash, notaryHash, 2*feePerKey, &notary.OnNEP17PaymentData{})
    83  
    84  	// `onPayment`: good
    85  	gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, 2*feePerKey, &notary.OnNEP17PaymentData{Till: uint32(depositLock)})
    86  	checkBalanceOf(t, notaryHash, 2*feePerKey)
    87  
    88  	// `expirationOf`: check `till` was set
    89  	notaryCommitteeInvoker.Invoke(t, depositLock, "expirationOf", multisigHash)
    90  
    91  	// `balanceOf`: check deposited amount for the multisig account
    92  	notaryCommitteeInvoker.Invoke(t, 2*feePerKey, "balanceOf", multisigHash)
    93  
    94  	// `onPayment`: good second deposit and explicit `to` paramenter
    95  	gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, feePerKey, &notary.OnNEP17PaymentData{Account: &multisigHash, Till: uint32(depositLock + 1)})
    96  	checkBalanceOf(t, notaryHash, 3*feePerKey)
    97  
    98  	// `balanceOf`: check deposited amount for the multisig account
    99  	notaryCommitteeInvoker.Invoke(t, 3*feePerKey, "balanceOf", multisigHash)
   100  
   101  	// `expirationOf`: check `till` is updated.
   102  	notaryCommitteeInvoker.Invoke(t, depositLock+1, "expirationOf", multisigHash)
   103  
   104  	// `onPayment`: empty payment, should fail because `till` less than the previous one
   105  	gasCommitteeInvoker.InvokeFail(t, "`till` shouldn't be less than the previous value", "transfer", multisigHash, notaryHash, int64(0), &notary.OnNEP17PaymentData{Account: &multisigHash, Till: uint32(depositLock)})
   106  	checkBalanceOf(t, notaryHash, 3*feePerKey)
   107  	notaryCommitteeInvoker.Invoke(t, depositLock+1, "expirationOf", multisigHash)
   108  
   109  	// `onPayment`: empty payment, should fail because `till` less than the chain height
   110  	gasCommitteeInvoker.InvokeFail(t, "`till` shouldn't be less than the chain's height", "transfer", multisigHash, notaryHash, int64(0), &notary.OnNEP17PaymentData{Account: &multisigHash, Till: uint32(1)})
   111  	checkBalanceOf(t, notaryHash, 3*feePerKey)
   112  	notaryCommitteeInvoker.Invoke(t, depositLock+1, "expirationOf", multisigHash)
   113  
   114  	// `onPayment`: empty payment, should successfully update `till`
   115  	gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, int64(0), &notary.OnNEP17PaymentData{Account: &multisigHash, Till: uint32(depositLock + 2)})
   116  	checkBalanceOf(t, notaryHash, 3*feePerKey)
   117  	notaryCommitteeInvoker.Invoke(t, depositLock+2, "expirationOf", multisigHash)
   118  
   119  	// `lockDepositUntil`: bad witness
   120  	notaryCommitteeInvoker.Invoke(t, false, "lockDepositUntil", util.Uint160{1, 2, 3}, int64(depositLock+3))
   121  	notaryCommitteeInvoker.Invoke(t, depositLock+2, "expirationOf", multisigHash)
   122  
   123  	// `lockDepositUntil`: bad `till` (less than the previous one)
   124  	notaryCommitteeInvoker.Invoke(t, false, "lockDepositUntil", multisigHash, int64(depositLock+1))
   125  	notaryCommitteeInvoker.Invoke(t, depositLock+2, "expirationOf", multisigHash)
   126  
   127  	// `lockDepositUntil`: bad `till` (less than the chain's height)
   128  	notaryCommitteeInvoker.Invoke(t, false, "lockDepositUntil", multisigHash, int64(1))
   129  	notaryCommitteeInvoker.Invoke(t, depositLock+2, "expirationOf", multisigHash)
   130  
   131  	// `lockDepositUntil`: good `till`
   132  	notaryCommitteeInvoker.Invoke(t, true, "lockDepositUntil", multisigHash, int64(depositLock+3))
   133  	notaryCommitteeInvoker.Invoke(t, depositLock+3, "expirationOf", multisigHash)
   134  
   135  	// Create new account for the next test
   136  	notaryAccInvoker := notaryCommitteeInvoker.WithSigners(e.NewAccount(t))
   137  	accHash := notaryAccInvoker.Signers[0].ScriptHash()
   138  
   139  	// `withdraw`: bad witness
   140  	notaryAccInvoker.Invoke(t, false, "withdraw", multisigHash, accHash)
   141  	notaryCommitteeInvoker.Invoke(t, 3*feePerKey, "balanceOf", multisigHash)
   142  
   143  	// `withdraw`: locked deposit
   144  	notaryCommitteeInvoker.Invoke(t, false, "withdraw", multisigHash, multisigHash)
   145  	notaryCommitteeInvoker.Invoke(t, 3*feePerKey, "balanceOf", multisigHash)
   146  
   147  	// `withdraw`: unlock deposit and transfer GAS back to owner
   148  	e.GenerateNewBlocks(t, depositLock)
   149  	notaryCommitteeInvoker.Invoke(t, true, "withdraw", multisigHash, accHash)
   150  	notaryCommitteeInvoker.Invoke(t, 0, "balanceOf", multisigHash)
   151  	checkBalanceOf(t, notaryHash, 0)
   152  
   153  	// `withdraw`:  the second time it should fail, because there's no deposit left
   154  	notaryCommitteeInvoker.Invoke(t, false, "withdraw", multisigHash, accHash)
   155  
   156  	// `onPayment`: good first deposit to other account, should set default `till` even if other `till` value is provided
   157  	gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, 2*feePerKey, &notary.OnNEP17PaymentData{Account: &accHash, Till: uint32(math.MaxUint32 - 1)})
   158  	checkBalanceOf(t, notaryHash, 2*feePerKey)
   159  	notaryCommitteeInvoker.Invoke(t, 5760+e.Chain.BlockHeight()-1, "expirationOf", accHash)
   160  
   161  	// `onPayment`: good second deposit to other account, shouldn't update `till` even if other `till` value is provided
   162  	gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, feePerKey, &notary.OnNEP17PaymentData{Account: &accHash, Till: uint32(math.MaxUint32 - 1)})
   163  	checkBalanceOf(t, notaryHash, 3*feePerKey)
   164  	notaryCommitteeInvoker.Invoke(t, 5760+e.Chain.BlockHeight()-3, "expirationOf", accHash)
   165  }
   166  
   167  func TestNotary_MaliciousWithdrawal(t *testing.T) {
   168  	const defaultDepositDeltaTill = 5760
   169  	notaryCommitteeInvoker := newNotaryClient(t)
   170  	e := notaryCommitteeInvoker.Executor
   171  	gasCommitteeInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Gas))
   172  
   173  	notaryHash := notaryCommitteeInvoker.NativeHash(t, nativenames.Notary)
   174  	feePerKey := e.Chain.GetNotaryServiceFeePerKey()
   175  	multisigHash := notaryCommitteeInvoker.Validator.ScriptHash() // matches committee's one for single chain
   176  
   177  	checkBalanceOf := func(t *testing.T, acc util.Uint160, expected int64) { // we don't have big numbers in this test, thus may use int
   178  		notaryCommitteeInvoker.CheckGASBalance(t, acc, big.NewInt(expected))
   179  	}
   180  
   181  	// Check Notary contract has no GAS on the account.
   182  	checkBalanceOf(t, notaryHash, 0)
   183  
   184  	// Perform several deposits to a set of different accounts.
   185  	count := 3
   186  	for i := 0; i < count; i++ {
   187  		h := random.Uint160()
   188  		gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, 2*feePerKey, &notary.OnNEP17PaymentData{Account: &h, Till: e.Chain.BlockHeight() + 2})
   189  	}
   190  	checkBalanceOf(t, notaryHash, int64(count)*2*feePerKey)
   191  
   192  	// Deploy malicious contract and make Notary deposit for it.
   193  	src := `package foo
   194  		import (
   195  			"github.com/nspcc-dev/neo-go/pkg/interop/native/notary"
   196  			"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
   197  			"github.com/nspcc-dev/neo-go/pkg/interop"
   198  			"github.com/nspcc-dev/neo-go/pkg/interop/storage"
   199  		)
   200  		const (
   201  			prefixMaxCallDepth = 0x01
   202  			defaultCallDepth = 3
   203  		)
   204  		func _deploy(_ any, isUpdate bool) {
   205  			ctx := storage.GetContext()
   206  			storage.Put(ctx, prefixMaxCallDepth, defaultCallDepth)
   207  		}
   208  		func OnNEP17Payment(from interop.Hash160, amount int, data any) {
   209  			ctx := storage.GetContext()
   210  			depth := storage.Get(ctx, prefixMaxCallDepth).(int)
   211  			if depth > 0 {
   212  				storage.Put(ctx, prefixMaxCallDepth, depth-1)
   213  				notary.Withdraw(runtime.GetExecutingScriptHash(), runtime.GetExecutingScriptHash())
   214  			}
   215  		}
   216  		func Verify() bool {
   217  			return true
   218  		}`
   219  	ctr := neotest.CompileSource(t, e.CommitteeHash, strings.NewReader(src), &compiler.Options{Name: "Helper", Permissions: []manifest.Permission{{
   220  		Methods: manifest.WildStrings{},
   221  	}}})
   222  	e.DeployContract(t, ctr, nil)
   223  	gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, 2*feePerKey, &notary.OnNEP17PaymentData{Account: &ctr.Hash, Till: e.Chain.BlockHeight() + 2})
   224  	checkBalanceOf(t, notaryHash, int64(count+1)*2*feePerKey)
   225  	depositLock := e.Chain.BlockHeight() + defaultDepositDeltaTill
   226  
   227  	// Try to perform malicious withdrawal from Notary contract. In malicious scenario the withdrawal cycle
   228  	// will be triggered by the contract itself (i.e. by malicious caller of the malicious contract), but
   229  	// for the test simplicity we'll use committee account.
   230  	for e.Chain.BlockHeight() <= depositLock { // side account made Notary deposit for contract, thus, default deposit delta till is used.
   231  		e.AddNewBlock(t)
   232  	}
   233  	maliciousInvoker := e.NewInvoker(notaryHash, notaryCommitteeInvoker.Signers[0], neotest.NewContractSigner(ctr.Hash, func(tx *transaction.Transaction) []any {
   234  		return nil
   235  	}))
   236  	maliciousInvoker.Invoke(t, true, "withdraw", ctr.Hash, ctr.Hash)
   237  	checkBalanceOf(t, notaryHash, int64(count)*2*feePerKey)
   238  	gasCommitteeInvoker.CheckGASBalance(t, ctr.Hash, big.NewInt(2*feePerKey))
   239  }
   240  
   241  func TestNotary_NotaryNodesReward(t *testing.T) {
   242  	checkReward := func(nKeys int, nNotaryNodes int, spendFullDeposit bool) {
   243  		notaryCommitteeInvoker := newNotaryClient(t)
   244  		e := notaryCommitteeInvoker.Executor
   245  		gasCommitteeInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Gas))
   246  		designationCommitteeInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Designation))
   247  
   248  		notaryHash := notaryCommitteeInvoker.NativeHash(t, nativenames.Notary)
   249  		feePerKey := e.Chain.GetNotaryServiceFeePerKey()
   250  		multisigHash := notaryCommitteeInvoker.Validator.ScriptHash() // matches committee's one for single chain
   251  
   252  		var err error
   253  
   254  		// set Notary nodes and check their balance
   255  		notaryNodes := make([]*keys.PrivateKey, nNotaryNodes)
   256  		notaryNodesPublicKeys := make([]any, nNotaryNodes)
   257  		for i := range notaryNodes {
   258  			notaryNodes[i], err = keys.NewPrivateKey()
   259  			require.NoError(t, err)
   260  			notaryNodesPublicKeys[i] = notaryNodes[i].PublicKey().Bytes()
   261  		}
   262  
   263  		designationCommitteeInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", int(noderoles.P2PNotary), notaryNodesPublicKeys)
   264  		for _, notaryNode := range notaryNodes {
   265  			e.CheckGASBalance(t, notaryNode.GetScriptHash(), big.NewInt(0))
   266  		}
   267  
   268  		// deposit GAS for `signer` with lock until the next block inclusively
   269  		depositAmount := 100_0000 + (2+int64(nKeys))*feePerKey // sysfee + netfee of the next transaction
   270  		if !spendFullDeposit {
   271  			depositAmount += 1_0000
   272  		}
   273  		gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, depositAmount, &notary.OnNEP17PaymentData{Account: &multisigHash, Till: e.Chain.BlockHeight() + 2})
   274  
   275  		// send transaction with Notary contract as a sender
   276  		tx := transaction.New([]byte{byte(opcode.PUSH1)}, 1_000_000)
   277  		tx.Nonce = neotest.Nonce()
   278  		tx.ValidUntilBlock = e.Chain.BlockHeight() + 1
   279  		tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: uint8(nKeys)}})
   280  		tx.NetworkFee = (2 + int64(nKeys)) * feePerKey
   281  		tx.Signers = []transaction.Signer{
   282  			{
   283  				Account: notaryHash,
   284  				Scopes:  transaction.None,
   285  			},
   286  			{
   287  				Account: multisigHash,
   288  				Scopes:  transaction.None,
   289  			},
   290  		}
   291  		tx.Scripts = []transaction.Witness{
   292  			{
   293  				InvocationScript: append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, notaryNodes[0].SignHashable(uint32(e.Chain.GetConfig().Magic), tx)...),
   294  			},
   295  			{
   296  				InvocationScript:   e.Committee.SignHashable(uint32(e.Chain.GetConfig().Magic), tx),
   297  				VerificationScript: e.Committee.Script(),
   298  			},
   299  		}
   300  		e.AddNewBlock(t, tx)
   301  
   302  		e.CheckGASBalance(t, notaryHash, big.NewInt(int64(depositAmount-tx.SystemFee-tx.NetworkFee)))
   303  		for _, notaryNode := range notaryNodes {
   304  			e.CheckGASBalance(t, notaryNode.GetScriptHash(), big.NewInt(feePerKey*int64((nKeys+1))/int64(nNotaryNodes)))
   305  		}
   306  	}
   307  
   308  	for _, spendDeposit := range []bool{true, false} {
   309  		checkReward(0, 1, spendDeposit)
   310  		checkReward(0, 2, spendDeposit)
   311  		checkReward(1, 1, spendDeposit)
   312  		checkReward(1, 2, spendDeposit)
   313  		checkReward(1, 3, spendDeposit)
   314  		checkReward(5, 1, spendDeposit)
   315  		checkReward(5, 2, spendDeposit)
   316  		checkReward(5, 6, spendDeposit)
   317  		checkReward(5, 7, spendDeposit)
   318  	}
   319  }