github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/neotest/basic.go (about) 1 package neotest 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "math/big" 7 "strings" 8 "testing" 9 10 "github.com/nspcc-dev/neo-go/pkg/config/netmode" 11 "github.com/nspcc-dev/neo-go/pkg/core" 12 "github.com/nspcc-dev/neo-go/pkg/core/block" 13 "github.com/nspcc-dev/neo-go/pkg/core/fee" 14 "github.com/nspcc-dev/neo-go/pkg/core/native" 15 "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" 16 "github.com/nspcc-dev/neo-go/pkg/core/state" 17 "github.com/nspcc-dev/neo-go/pkg/core/transaction" 18 "github.com/nspcc-dev/neo-go/pkg/io" 19 "github.com/nspcc-dev/neo-go/pkg/smartcontract" 20 "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" 21 "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" 22 "github.com/nspcc-dev/neo-go/pkg/util" 23 "github.com/nspcc-dev/neo-go/pkg/vm" 24 "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" 25 "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" 26 "github.com/nspcc-dev/neo-go/pkg/wallet" 27 "github.com/stretchr/testify/require" 28 ) 29 30 // Executor is a wrapper over chain state. 31 type Executor struct { 32 Chain *core.Blockchain 33 Validator Signer 34 Committee Signer 35 CommitteeHash util.Uint160 36 Contracts map[string]*Contract 37 } 38 39 // NewExecutor creates a new executor instance from the provided blockchain and committee. 40 func NewExecutor(t testing.TB, bc *core.Blockchain, validator, committee Signer) *Executor { 41 checkMultiSigner(t, validator) 42 checkMultiSigner(t, committee) 43 44 return &Executor{ 45 Chain: bc, 46 Validator: validator, 47 Committee: committee, 48 CommitteeHash: committee.ScriptHash(), 49 Contracts: make(map[string]*Contract), 50 } 51 } 52 53 // TopBlock returns the block with the highest index. 54 func (e *Executor) TopBlock(t testing.TB) *block.Block { 55 b, err := e.Chain.GetBlock(e.Chain.GetHeaderHash(e.Chain.BlockHeight())) 56 require.NoError(t, err) 57 return b 58 } 59 60 // NativeHash returns a native contract hash by the name. 61 func (e *Executor) NativeHash(t testing.TB, name string) util.Uint160 { 62 h, err := e.Chain.GetNativeContractScriptHash(name) 63 require.NoError(t, err) 64 return h 65 } 66 67 // ContractHash returns a contract hash by the ID. 68 func (e *Executor) ContractHash(t testing.TB, id int32) util.Uint160 { 69 h, err := e.Chain.GetContractScriptHash(id) 70 require.NoError(t, err) 71 return h 72 } 73 74 // NativeID returns a native contract ID by the name. 75 func (e *Executor) NativeID(t testing.TB, name string) int32 { 76 h := e.NativeHash(t, name) 77 cs := e.Chain.GetContractState(h) 78 require.NotNil(t, cs) 79 return cs.ID 80 } 81 82 // NewUnsignedTx creates a new unsigned transaction which invokes the method of the contract with the hash. 83 func (e *Executor) NewUnsignedTx(t testing.TB, hash util.Uint160, method string, args ...any) *transaction.Transaction { 84 script, err := smartcontract.CreateCallScript(hash, method, args...) 85 require.NoError(t, err) 86 87 tx := transaction.New(script, 0) 88 tx.Nonce = Nonce() 89 tx.ValidUntilBlock = e.Chain.BlockHeight() + 1 90 return tx 91 } 92 93 // NewTx creates a new transaction which invokes the contract method. 94 // The transaction is signed by the signers. 95 func (e *Executor) NewTx(t testing.TB, signers []Signer, 96 hash util.Uint160, method string, args ...any) *transaction.Transaction { 97 tx := e.NewUnsignedTx(t, hash, method, args...) 98 return e.SignTx(t, tx, -1, signers...) 99 } 100 101 // SignTx signs a transaction using the provided signers. 102 func (e *Executor) SignTx(t testing.TB, tx *transaction.Transaction, sysFee int64, signers ...Signer) *transaction.Transaction { 103 for _, acc := range signers { 104 tx.Signers = append(tx.Signers, transaction.Signer{ 105 Account: acc.ScriptHash(), 106 Scopes: transaction.Global, 107 }) 108 } 109 AddNetworkFee(t, e.Chain, tx, signers...) 110 AddSystemFee(e.Chain, tx, sysFee) 111 112 for _, acc := range signers { 113 require.NoError(t, acc.SignTx(e.Chain.GetConfig().Magic, tx)) 114 } 115 return tx 116 } 117 118 // NewAccount returns a new signer holding 100.0 GAS (or given amount is specified). 119 // This method advances the chain by one block with a transfer transaction. 120 func (e *Executor) NewAccount(t testing.TB, expectedGASBalance ...int64) Signer { 121 acc, err := wallet.NewAccount() 122 require.NoError(t, err) 123 124 amount := int64(100_0000_0000) 125 if len(expectedGASBalance) != 0 { 126 amount = expectedGASBalance[0] 127 } 128 tx := e.NewTx(t, []Signer{e.Validator}, 129 e.NativeHash(t, nativenames.Gas), "transfer", 130 e.Validator.ScriptHash(), acc.Contract.ScriptHash(), amount, nil) 131 e.AddNewBlock(t, tx) 132 e.CheckHalt(t, tx.Hash()) 133 return NewSingleSigner(acc) 134 } 135 136 // DeployContract compiles and deploys a contract to the bc. It also checks that 137 // the precalculated contract hash matches the actual one. 138 // data is an optional argument to `_deploy`. 139 // It returns the hash of the deploy transaction. 140 func (e *Executor) DeployContract(t testing.TB, c *Contract, data any) util.Uint256 { 141 return e.DeployContractBy(t, e.Validator, c, data) 142 } 143 144 // DeployContractBy compiles and deploys a contract to the bc using the provided signer. 145 // It also checks that the precalculated contract hash matches the actual one. 146 // data is an optional argument to `_deploy`. 147 // It returns the hash of the deploy transaction. 148 func (e *Executor) DeployContractBy(t testing.TB, signer Signer, c *Contract, data any) util.Uint256 { 149 tx := NewDeployTxBy(t, e.Chain, signer, c, data) 150 e.AddNewBlock(t, tx) 151 e.CheckHalt(t, tx.Hash()) 152 153 // Check that the precalculated hash matches the real one. 154 e.CheckTxNotificationEvent(t, tx.Hash(), -1, state.NotificationEvent{ 155 ScriptHash: e.NativeHash(t, nativenames.Management), 156 Name: "Deploy", 157 Item: stackitem.NewArray([]stackitem.Item{ 158 stackitem.NewByteArray(c.Hash.BytesBE()), 159 }), 160 }) 161 162 return tx.Hash() 163 } 164 165 // DeployContractCheckFAULT compiles and deploys a contract to the bc using the validator 166 // account. It checks that the deploy transaction FAULTed with the specified error. 167 func (e *Executor) DeployContractCheckFAULT(t testing.TB, c *Contract, data any, errMessage string) { 168 tx := e.NewDeployTx(t, e.Chain, c, data) 169 e.AddNewBlock(t, tx) 170 e.CheckFault(t, tx.Hash(), errMessage) 171 } 172 173 // InvokeScript adds a transaction with the specified script to the chain and 174 // returns its hash. It does no faults check. 175 func (e *Executor) InvokeScript(t testing.TB, script []byte, signers []Signer) util.Uint256 { 176 tx := e.PrepareInvocation(t, script, signers) 177 e.AddNewBlock(t, tx) 178 return tx.Hash() 179 } 180 181 // PrepareInvocation creates a transaction with the specified script and signs it 182 // by the provided signer. 183 func (e *Executor) PrepareInvocation(t testing.TB, script []byte, signers []Signer, validUntilBlock ...uint32) *transaction.Transaction { 184 tx := e.PrepareInvocationNoSign(t, script, validUntilBlock...) 185 e.SignTx(t, tx, -1, signers...) 186 return tx 187 } 188 189 func (e *Executor) PrepareInvocationNoSign(t testing.TB, script []byte, validUntilBlock ...uint32) *transaction.Transaction { 190 tx := transaction.New(script, 0) 191 tx.Nonce = Nonce() 192 tx.ValidUntilBlock = e.Chain.BlockHeight() + 1 193 if len(validUntilBlock) != 0 { 194 tx.ValidUntilBlock = validUntilBlock[0] 195 } 196 return tx 197 } 198 199 // InvokeScriptCheckHALT adds a transaction with the specified script to the chain 200 // and checks if it's HALTed with the specified items on stack. 201 func (e *Executor) InvokeScriptCheckHALT(t testing.TB, script []byte, signers []Signer, stack ...stackitem.Item) { 202 hash := e.InvokeScript(t, script, signers) 203 e.CheckHalt(t, hash, stack...) 204 } 205 206 // InvokeScriptCheckFAULT adds a transaction with the specified script to the 207 // chain and checks if it's FAULTed with the specified error. 208 func (e *Executor) InvokeScriptCheckFAULT(t testing.TB, script []byte, signers []Signer, errMessage string) util.Uint256 { 209 hash := e.InvokeScript(t, script, signers) 210 e.CheckFault(t, hash, errMessage) 211 return hash 212 } 213 214 // CheckHalt checks that the transaction is persisted with HALT state. 215 func (e *Executor) CheckHalt(t testing.TB, h util.Uint256, stack ...stackitem.Item) *state.AppExecResult { 216 aer, err := e.Chain.GetAppExecResults(h, trigger.Application) 217 require.NoError(t, err) 218 require.Equal(t, vmstate.Halt, aer[0].VMState, aer[0].FaultException) 219 if len(stack) != 0 { 220 require.Equal(t, stack, aer[0].Stack) 221 } 222 return &aer[0] 223 } 224 225 // CheckFault checks that the transaction is persisted with FAULT state. 226 // The raised exception is also checked to contain the s as a substring. 227 func (e *Executor) CheckFault(t testing.TB, h util.Uint256, s string) { 228 aer, err := e.Chain.GetAppExecResults(h, trigger.Application) 229 require.NoError(t, err) 230 require.Equal(t, vmstate.Fault, aer[0].VMState) 231 require.True(t, strings.Contains(aer[0].FaultException, s), 232 "expected: %s, got: %s", s, aer[0].FaultException) 233 } 234 235 // CheckTxNotificationEvent checks that the specified event was emitted at the specified position 236 // during transaction script execution. Negative index corresponds to backwards enumeration. 237 func (e *Executor) CheckTxNotificationEvent(t testing.TB, h util.Uint256, index int, expected state.NotificationEvent) { 238 aer, err := e.Chain.GetAppExecResults(h, trigger.Application) 239 require.NoError(t, err) 240 l := len(aer[0].Events) 241 if index < 0 { 242 index = l + index 243 } 244 require.True(t, 0 <= index && index < l, fmt.Errorf("notification index is out of range: want %d, len is %d", index, l)) 245 require.Equal(t, expected, aer[0].Events[index]) 246 } 247 248 // CheckGASBalance ensures that the provided account owns the specified amount of GAS. 249 func (e *Executor) CheckGASBalance(t testing.TB, acc util.Uint160, expected *big.Int) { 250 actual := e.Chain.GetUtilityTokenBalance(acc) 251 require.Equal(t, expected, actual, fmt.Errorf("invalid GAS balance: expected %s, got %s", expected.String(), actual.String())) 252 } 253 254 // EnsureGASBalance ensures that the provided account owns the amount of GAS that satisfies the provided condition. 255 func (e *Executor) EnsureGASBalance(t testing.TB, acc util.Uint160, isOk func(balance *big.Int) bool) { 256 actual := e.Chain.GetUtilityTokenBalance(acc) 257 require.True(t, isOk(actual), fmt.Errorf("invalid GAS balance: got %s, condition is not satisfied", actual.String())) 258 } 259 260 // NewDeployTx returns a new deployment tx for the contract signed by the committee. 261 func (e *Executor) NewDeployTx(t testing.TB, bc *core.Blockchain, c *Contract, data any) *transaction.Transaction { 262 return NewDeployTxBy(t, bc, e.Validator, c, data) 263 } 264 265 // NewDeployTxBy returns a new deployment tx for the contract signed by the specified signer. 266 func NewDeployTxBy(t testing.TB, bc *core.Blockchain, signer Signer, c *Contract, data any) *transaction.Transaction { 267 rawManifest, err := json.Marshal(c.Manifest) 268 require.NoError(t, err) 269 270 neb, err := c.NEF.Bytes() 271 require.NoError(t, err) 272 273 script, err := smartcontract.CreateCallScript(bc.ManagementContractHash(), "deploy", neb, rawManifest, data) 274 require.NoError(t, err) 275 276 tx := transaction.New(script, 100*native.GASFactor) 277 tx.Nonce = Nonce() 278 tx.ValidUntilBlock = bc.BlockHeight() + 1 279 tx.Signers = []transaction.Signer{{ 280 Account: signer.ScriptHash(), 281 Scopes: transaction.Global, 282 }} 283 AddNetworkFee(t, bc, tx, signer) 284 require.NoError(t, signer.SignTx(netmode.UnitTestNet, tx)) 285 return tx 286 } 287 288 // AddSystemFee adds system fee to the transaction. If negative value specified, 289 // then system fee is defined by test invocation. 290 func AddSystemFee(bc *core.Blockchain, tx *transaction.Transaction, sysFee int64) { 291 if sysFee >= 0 { 292 tx.SystemFee = sysFee 293 return 294 } 295 v, _ := TestInvoke(bc, tx) // ignore error to support failing transactions 296 tx.SystemFee = v.GasConsumed() 297 } 298 299 // AddNetworkFee adds network fee to the transaction. 300 func AddNetworkFee(t testing.TB, bc *core.Blockchain, tx *transaction.Transaction, signers ...Signer) { 301 baseFee := bc.GetBaseExecFee() 302 size := io.GetVarSize(tx) 303 for _, sgr := range signers { 304 csgr, ok := sgr.(SingleSigner) 305 if ok && csgr.Account().Contract.InvocationBuilder != nil { 306 sc, err := csgr.Account().Contract.InvocationBuilder(tx) 307 require.NoError(t, err) 308 309 txCopy := *tx 310 ic, err := bc.GetTestVM(trigger.Verification, &txCopy, nil) 311 require.NoError(t, err) 312 313 ic.UseSigners(tx.Signers) 314 ic.VM.GasLimit = bc.GetMaxVerificationGAS() 315 316 require.NoError(t, bc.InitVerificationContext(ic, csgr.ScriptHash(), &transaction.Witness{InvocationScript: sc, VerificationScript: csgr.Script()})) 317 require.NoError(t, ic.VM.Run()) 318 319 tx.NetworkFee += ic.VM.GasConsumed() 320 size += io.GetVarSize(sc) + io.GetVarSize(csgr.Script()) 321 } else { 322 netFee, sizeDelta := fee.Calculate(baseFee, sgr.Script()) 323 tx.NetworkFee += netFee 324 size += sizeDelta 325 } 326 } 327 tx.NetworkFee += int64(size)*bc.FeePerByte() + bc.CalculateAttributesFee(tx) 328 } 329 330 // NewUnsignedBlock creates a new unsigned block from txs. 331 func (e *Executor) NewUnsignedBlock(t testing.TB, txs ...*transaction.Transaction) *block.Block { 332 lastBlock := e.TopBlock(t) 333 b := &block.Block{ 334 Header: block.Header{ 335 NextConsensus: e.Validator.ScriptHash(), 336 Script: transaction.Witness{ 337 VerificationScript: e.Validator.Script(), 338 }, 339 Timestamp: lastBlock.Timestamp + 1, 340 }, 341 Transactions: txs, 342 } 343 if e.Chain.GetConfig().StateRootInHeader { 344 b.StateRootEnabled = true 345 b.PrevStateRoot = e.Chain.GetStateModule().CurrentLocalStateRoot() 346 } 347 b.PrevHash = lastBlock.Hash() 348 b.Index = e.Chain.BlockHeight() + 1 349 b.RebuildMerkleRoot() 350 return b 351 } 352 353 // AddNewBlock creates a new block from the provided transactions and adds it on the bc. 354 func (e *Executor) AddNewBlock(t testing.TB, txs ...*transaction.Transaction) *block.Block { 355 b := e.NewUnsignedBlock(t, txs...) 356 e.SignBlock(b) 357 require.NoError(t, e.Chain.AddBlock(b)) 358 return b 359 } 360 361 // GenerateNewBlocks adds the specified number of empty blocks to the chain. 362 func (e *Executor) GenerateNewBlocks(t testing.TB, count int) []*block.Block { 363 blocks := make([]*block.Block, count) 364 for i := 0; i < count; i++ { 365 blocks[i] = e.AddNewBlock(t) 366 } 367 return blocks 368 } 369 370 // SignBlock add validators signature to b. 371 func (e *Executor) SignBlock(b *block.Block) *block.Block { 372 invoc := e.Validator.SignHashable(uint32(e.Chain.GetConfig().Magic), b) 373 b.Script.InvocationScript = invoc 374 return b 375 } 376 377 // AddBlockCheckHalt is a convenient wrapper over AddBlock and CheckHalt. 378 func (e *Executor) AddBlockCheckHalt(t testing.TB, txs ...*transaction.Transaction) *block.Block { 379 b := e.AddNewBlock(t, txs...) 380 for _, tx := range txs { 381 e.CheckHalt(t, tx.Hash()) 382 } 383 return b 384 } 385 386 // TestInvoke creates a test VM with a dummy block and executes a transaction in it. 387 func TestInvoke(bc *core.Blockchain, tx *transaction.Transaction) (*vm.VM, error) { 388 lastBlock, err := bc.GetBlock(bc.GetHeaderHash(bc.BlockHeight())) 389 if err != nil { 390 return nil, err 391 } 392 b := &block.Block{ 393 Header: block.Header{ 394 Index: bc.BlockHeight() + 1, 395 Timestamp: lastBlock.Timestamp + 1, 396 }, 397 } 398 399 // `GetTestVM` as well as `Run` can use a transaction hash which will set a cached value. 400 // This is unwanted behavior, so we explicitly copy the transaction to perform execution. 401 ttx := *tx 402 ic, _ := bc.GetTestVM(trigger.Application, &ttx, b) 403 404 defer ic.Finalize() 405 406 ic.VM.LoadWithFlags(tx.Script, callflag.All) 407 err = ic.VM.Run() 408 return ic.VM, err 409 } 410 411 // GetTransaction returns a transaction and its height by the specified hash. 412 func (e *Executor) GetTransaction(t testing.TB, h util.Uint256) (*transaction.Transaction, uint32) { 413 tx, height, err := e.Chain.GetTransaction(h) 414 require.NoError(t, err) 415 return tx, height 416 } 417 418 // GetBlockByIndex returns a block by the specified index. 419 func (e *Executor) GetBlockByIndex(t testing.TB, idx uint32) *block.Block { 420 h := e.Chain.GetHeaderHash(idx) 421 require.NotEmpty(t, h) 422 b, err := e.Chain.GetBlock(h) 423 require.NoError(t, err) 424 return b 425 } 426 427 // GetTxExecResult returns application execution results for the specified transaction. 428 func (e *Executor) GetTxExecResult(t testing.TB, h util.Uint256) *state.AppExecResult { 429 aer, err := e.Chain.GetAppExecResults(h, trigger.Application) 430 require.NoError(t, err) 431 require.Equal(t, 1, len(aer)) 432 return &aer[0] 433 }