package core import ( "math" "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 := 100 // 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)) // `onPayment`: good first deposit to other account, should set default `till` even if other `till` value is provided transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 2*transaction.NotaryServiceFeePerKey, acc.PrivateKey().PublicKey().GetScriptHash(), int64(math.MaxUint32-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, 2*transaction.NotaryServiceFeePerKey) till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", acc.PrivateKey().PublicKey().GetScriptHash()) require.NoError(t, err) checkResult(t, till, stackitem.Make(5760+chain.BlockHeight()-2)) // `onPayment`: good second deposit to other account, shouldn't update `till` even if other `till` value is provided transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 2*transaction.NotaryServiceFeePerKey, acc.PrivateKey().PublicKey().GetScriptHash(), int64(math.MaxUint32-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, 4*transaction.NotaryServiceFeePerKey) till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", acc.PrivateKey().PublicKey().GetScriptHash()) require.NoError(t, err) checkResult(t, till, stackitem.Make(5760+chain.BlockHeight()-4)) } 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) } }