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), ¬ary.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), ¬ary.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, ¬ary.OnNEP17PaymentData{}) 83 84 // `onPayment`: good 85 gasCommitteeInvoker.Invoke(t, true, "transfer", multisigHash, notaryHash, 2*feePerKey, ¬ary.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, ¬ary.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), ¬ary.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), ¬ary.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), ¬ary.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, ¬ary.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, ¬ary.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, ¬ary.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, ¬ary.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, ¬ary.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 }