forked from TrueCloudLab/neoneo-go
4c39b6600d
Two times less keys inserted into the DB per tx leads to ~13% TPS improvement. We also drop one goroutine with it which isn't bad as well.
1829 lines
64 KiB
Go
1829 lines
64 KiB
Go
package core
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"math/rand"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nspcc-dev/neo-go/internal/random"
|
|
"github.com/nspcc-dev/neo-go/internal/testchain"
|
|
"github.com/nspcc-dev/neo-go/pkg/config"
|
|
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/blockchainer"
|
|
"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/interop/interopnames"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/mempool"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/native/nativeprices"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
|
|
"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/crypto/keys"
|
|
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result/subscriptions"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
"github.com/nspcc-dev/neo-go/pkg/util/slice"
|
|
"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/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap/zaptest"
|
|
)
|
|
|
|
func TestVerifyHeader(t *testing.T) {
|
|
bc := newTestChain(t)
|
|
prev := bc.topBlock.Load().(*block.Block).Header
|
|
t.Run("Invalid", func(t *testing.T) {
|
|
t.Run("Hash", func(t *testing.T) {
|
|
h := prev.Hash()
|
|
h[0] = ^h[0]
|
|
hdr := newBlock(bc.config, 1, h).Header
|
|
require.True(t, errors.Is(bc.verifyHeader(&hdr, &prev), ErrHdrHashMismatch))
|
|
})
|
|
t.Run("Index", func(t *testing.T) {
|
|
hdr := newBlock(bc.config, 3, prev.Hash()).Header
|
|
require.True(t, errors.Is(bc.verifyHeader(&hdr, &prev), ErrHdrIndexMismatch))
|
|
})
|
|
t.Run("Timestamp", func(t *testing.T) {
|
|
hdr := newBlock(bc.config, 1, prev.Hash()).Header
|
|
hdr.Timestamp = 0
|
|
require.True(t, errors.Is(bc.verifyHeader(&hdr, &prev), ErrHdrInvalidTimestamp))
|
|
})
|
|
})
|
|
t.Run("Valid", func(t *testing.T) {
|
|
hdr := newBlock(bc.config, 1, prev.Hash()).Header
|
|
require.NoError(t, bc.verifyHeader(&hdr, &prev))
|
|
})
|
|
}
|
|
|
|
func TestAddHeaders(t *testing.T) {
|
|
bc := newTestChain(t)
|
|
lastBlock := bc.topBlock.Load().(*block.Block)
|
|
h1 := newBlock(bc.config, 1, lastBlock.Hash()).Header
|
|
h2 := newBlock(bc.config, 2, h1.Hash()).Header
|
|
h3 := newBlock(bc.config, 3, h2.Hash()).Header
|
|
|
|
require.NoError(t, bc.AddHeaders())
|
|
require.NoError(t, bc.AddHeaders(&h1, &h2))
|
|
require.NoError(t, bc.AddHeaders(&h2, &h3))
|
|
|
|
assert.Equal(t, h3.Index, bc.HeaderHeight())
|
|
assert.Equal(t, uint32(0), bc.BlockHeight())
|
|
assert.Equal(t, h3.Hash(), bc.CurrentHeaderHash())
|
|
|
|
// Add them again, they should not be added.
|
|
require.NoError(t, bc.AddHeaders(&h3, &h2, &h1))
|
|
|
|
assert.Equal(t, h3.Index, bc.HeaderHeight())
|
|
assert.Equal(t, uint32(0), bc.BlockHeight())
|
|
assert.Equal(t, h3.Hash(), bc.CurrentHeaderHash())
|
|
|
|
h4 := newBlock(bc.config, 4, h3.Hash().Reverse()).Header
|
|
h5 := newBlock(bc.config, 5, h4.Hash()).Header
|
|
|
|
assert.Error(t, bc.AddHeaders(&h4, &h5))
|
|
assert.Equal(t, h3.Index, bc.HeaderHeight())
|
|
assert.Equal(t, uint32(0), bc.BlockHeight())
|
|
assert.Equal(t, h3.Hash(), bc.CurrentHeaderHash())
|
|
|
|
h6 := newBlock(bc.config, 4, h3.Hash()).Header
|
|
h6.Script.InvocationScript = nil
|
|
assert.Error(t, bc.AddHeaders(&h6))
|
|
assert.Equal(t, h3.Index, bc.HeaderHeight())
|
|
assert.Equal(t, uint32(0), bc.BlockHeight())
|
|
assert.Equal(t, h3.Hash(), bc.CurrentHeaderHash())
|
|
}
|
|
|
|
func TestAddBlock(t *testing.T) {
|
|
const size = 3
|
|
bc := newTestChain(t)
|
|
blocks, err := bc.genBlocks(size)
|
|
require.NoError(t, err)
|
|
|
|
lastBlock := blocks[len(blocks)-1]
|
|
assert.Equal(t, lastBlock.Index, bc.HeaderHeight())
|
|
assert.Equal(t, lastBlock.Hash(), bc.CurrentHeaderHash())
|
|
|
|
// This one tests persisting blocks, so it does need to persist()
|
|
_, err = bc.persist(false)
|
|
require.NoError(t, err)
|
|
|
|
for _, block := range blocks {
|
|
key := storage.AppendPrefix(storage.DataExecutable, block.Hash().BytesBE())
|
|
_, err := bc.dao.Store.Get(key)
|
|
require.NoErrorf(t, err, "block %s not persisted", block.Hash())
|
|
}
|
|
|
|
assert.Equal(t, lastBlock.Index, bc.BlockHeight())
|
|
assert.Equal(t, lastBlock.Hash(), bc.CurrentHeaderHash())
|
|
}
|
|
|
|
func TestAddBlockStateRoot(t *testing.T) {
|
|
bc := newTestChainWithCustomCfg(t, func(c *config.Config) {
|
|
c.ProtocolConfiguration.StateRootInHeader = true
|
|
})
|
|
|
|
sr, err := bc.GetStateModule().GetStateRoot(bc.BlockHeight())
|
|
require.NoError(t, err)
|
|
|
|
tx := newNEP17Transfer(bc.contracts.NEO.Hash, neoOwner, util.Uint160{}, 1)
|
|
tx.ValidUntilBlock = bc.BlockHeight() + 1
|
|
addSigners(neoOwner, tx)
|
|
require.NoError(t, testchain.SignTx(bc, tx))
|
|
|
|
lastBlock := bc.topBlock.Load().(*block.Block)
|
|
b := newBlock(bc.config, lastBlock.Index+1, lastBlock.Hash(), tx)
|
|
err = bc.AddBlock(b)
|
|
require.True(t, errors.Is(err, ErrHdrStateRootSetting), "got: %v", err)
|
|
|
|
u := sr.Root
|
|
u[0] ^= 0xFF
|
|
b = newBlockWithState(bc.config, lastBlock.Index+1, lastBlock.Hash(), &u, tx)
|
|
err = bc.AddBlock(b)
|
|
require.True(t, errors.Is(err, ErrHdrInvalidStateRoot), "got: %v", err)
|
|
|
|
b = bc.newBlock(tx)
|
|
require.NoError(t, bc.AddBlock(b))
|
|
}
|
|
|
|
func TestAddHeadersStateRoot(t *testing.T) {
|
|
bc := newTestChainWithCustomCfg(t, func(c *config.Config) {
|
|
c.ProtocolConfiguration.StateRootInHeader = true
|
|
})
|
|
|
|
r := bc.stateRoot.CurrentLocalStateRoot()
|
|
h1 := bc.newBlock().Header
|
|
|
|
// invalid stateroot
|
|
h1.PrevStateRoot[0] ^= 0xFF
|
|
require.True(t, errors.Is(bc.AddHeaders(&h1), ErrHdrInvalidStateRoot))
|
|
|
|
// valid stateroot
|
|
h1.PrevStateRoot = r
|
|
require.NoError(t, bc.AddHeaders(&h1))
|
|
|
|
// unable to verify stateroot (stateroot is computed for block #0 only => can
|
|
// verify stateroot of header #1 only) => just store the header
|
|
h2 := newBlockWithState(bc.config, 2, h1.Hash(), nil).Header
|
|
require.NoError(t, bc.AddHeaders(&h2))
|
|
}
|
|
|
|
func TestAddBadBlock(t *testing.T) {
|
|
bc := newTestChain(t)
|
|
// It has ValidUntilBlock == 0, which is wrong
|
|
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx.Signers = []transaction.Signer{{
|
|
Account: testchain.MultisigScriptHash(),
|
|
Scopes: transaction.None,
|
|
}}
|
|
require.NoError(t, testchain.SignTx(bc, tx))
|
|
b1 := bc.newBlock(tx)
|
|
|
|
require.Error(t, bc.AddBlock(b1))
|
|
bc.config.VerifyTransactions = false
|
|
require.NoError(t, bc.AddBlock(b1))
|
|
|
|
b2 := bc.newBlock()
|
|
b2.PrevHash = util.Uint256{}
|
|
|
|
require.Error(t, bc.AddBlock(b2))
|
|
bc.config.VerifyBlocks = false
|
|
require.NoError(t, bc.AddBlock(b2))
|
|
|
|
tx = transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx.ValidUntilBlock = 128
|
|
tx.Signers = []transaction.Signer{{
|
|
Account: testchain.MultisigScriptHash(),
|
|
Scopes: transaction.None,
|
|
}}
|
|
require.NoError(t, testchain.SignTx(bc, tx))
|
|
require.NoError(t, bc.PoolTx(tx))
|
|
bc.config.VerifyTransactions = true
|
|
bc.config.VerifyBlocks = true
|
|
b3 := bc.newBlock(tx)
|
|
require.NoError(t, bc.AddBlock(b3))
|
|
}
|
|
|
|
func TestGetHeader(t *testing.T) {
|
|
bc := newTestChain(t)
|
|
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx.ValidUntilBlock = bc.BlockHeight() + 1
|
|
addSigners(neoOwner, tx)
|
|
assert.Nil(t, testchain.SignTx(bc, tx))
|
|
block := bc.newBlock(tx)
|
|
err := bc.AddBlock(block)
|
|
assert.Nil(t, err)
|
|
|
|
// Test unpersisted and persisted access
|
|
for i := 0; i < 2; i++ {
|
|
hash := block.Hash()
|
|
header, err := bc.GetHeader(hash)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, &block.Header, header)
|
|
|
|
b2 := bc.newBlock()
|
|
_, err = bc.GetHeader(b2.Hash())
|
|
assert.Error(t, err)
|
|
_, err = bc.persist(false)
|
|
assert.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
func TestGetBlock(t *testing.T) {
|
|
bc := newTestChain(t)
|
|
blocks, err := bc.genBlocks(100)
|
|
require.NoError(t, err)
|
|
|
|
// Test unpersisted and persisted access
|
|
for j := 0; j < 2; j++ {
|
|
for i := 0; i < len(blocks); i++ {
|
|
block, err := bc.GetBlock(blocks[i].Hash())
|
|
require.NoErrorf(t, err, "can't get block %d: %s, attempt %d", i, err, j)
|
|
assert.Equal(t, blocks[i].Index, block.Index)
|
|
assert.Equal(t, blocks[i].Hash(), block.Hash())
|
|
}
|
|
_, err = bc.persist(false)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
t.Run("store only header", func(t *testing.T) {
|
|
t.Run("non-empty block", func(t *testing.T) {
|
|
tx, err := testchain.NewTransferFromOwner(bc, bc.contracts.NEO.Hash,
|
|
random.Uint160(), 1, 1, 1000)
|
|
require.NoError(t, err)
|
|
b := bc.newBlock(tx)
|
|
require.NoError(t, bc.AddHeaders(&b.Header))
|
|
|
|
_, err = bc.GetBlock(b.Hash())
|
|
require.Error(t, err)
|
|
|
|
_, err = bc.GetHeader(b.Hash())
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, bc.AddBlock(b))
|
|
|
|
_, err = bc.GetBlock(b.Hash())
|
|
require.NoError(t, err)
|
|
})
|
|
t.Run("empty block", func(t *testing.T) {
|
|
b := bc.newBlock()
|
|
require.NoError(t, bc.AddHeaders(&b.Header))
|
|
|
|
_, err = bc.GetBlock(b.Hash())
|
|
require.NoError(t, err)
|
|
})
|
|
})
|
|
}
|
|
|
|
func (bc *Blockchain) newTestTx(h util.Uint160, script []byte) *transaction.Transaction {
|
|
tx := transaction.New(script, 1_000_000)
|
|
tx.Nonce = rand.Uint32()
|
|
tx.ValidUntilBlock = 100
|
|
tx.Signers = []transaction.Signer{{
|
|
Account: h,
|
|
Scopes: transaction.CalledByEntry,
|
|
}}
|
|
tx.NetworkFee = int64(io.GetVarSize(tx)+200 /* witness */) * bc.FeePerByte()
|
|
tx.NetworkFee += 1_000_000 // verification cost
|
|
return tx
|
|
}
|
|
|
|
func TestVerifyTx(t *testing.T) {
|
|
bc := newTestChain(t)
|
|
|
|
accs := make([]*wallet.Account, 5)
|
|
for i := range accs {
|
|
var err error
|
|
accs[i], err = wallet.NewAccount()
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
oracleAcc := accs[2]
|
|
oraclePubs := keys.PublicKeys{oracleAcc.PrivateKey().PublicKey()}
|
|
require.NoError(t, oracleAcc.ConvertMultisig(1, oraclePubs))
|
|
|
|
neoHash := bc.contracts.NEO.Hash
|
|
gasHash := bc.contracts.GAS.Hash
|
|
w := io.NewBufBinWriter()
|
|
for _, sc := range []util.Uint160{neoHash, gasHash} {
|
|
for _, a := range accs {
|
|
amount := int64(1_000_000)
|
|
if sc.Equals(gasHash) {
|
|
amount = 1_000_000_000
|
|
}
|
|
emit.AppCall(w.BinWriter, sc, "transfer", callflag.All,
|
|
neoOwner, a.Contract.ScriptHash(), amount, nil)
|
|
emit.Opcodes(w.BinWriter, opcode.ASSERT)
|
|
}
|
|
}
|
|
emit.AppCall(w.BinWriter, gasHash, "transfer", callflag.All,
|
|
neoOwner, testchain.CommitteeScriptHash(), int64(1_000_000_000), nil)
|
|
emit.Opcodes(w.BinWriter, opcode.ASSERT)
|
|
require.NoError(t, w.Err)
|
|
|
|
txMove := bc.newTestTx(neoOwner, w.Bytes())
|
|
txMove.SystemFee = 1_000_000_000
|
|
require.NoError(t, testchain.SignTx(bc, txMove))
|
|
b := bc.newBlock(txMove)
|
|
require.NoError(t, bc.AddBlock(b))
|
|
|
|
aer, err := bc.GetAppExecResults(txMove.Hash(), trigger.Application)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(aer))
|
|
require.Equal(t, aer[0].VMState, vm.HaltState)
|
|
|
|
res, err := invokeContractMethodGeneric(bc, 100000000, bc.contracts.Policy.Hash, "blockAccount", true, accs[1].PrivateKey().GetScriptHash().BytesBE())
|
|
require.NoError(t, err)
|
|
checkResult(t, res, stackitem.NewBool(true))
|
|
|
|
checkErr := func(t *testing.T, expectedErr error, tx *transaction.Transaction) {
|
|
err := bc.VerifyTx(tx)
|
|
require.True(t, errors.Is(err, expectedErr), "expected: %v, got: %v", expectedErr, err)
|
|
}
|
|
|
|
testScript := []byte{byte(opcode.PUSH1)}
|
|
h := accs[0].PrivateKey().GetScriptHash()
|
|
t.Run("Expired", func(t *testing.T) {
|
|
tx := bc.newTestTx(h, testScript)
|
|
tx.ValidUntilBlock = 1
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
|
|
checkErr(t, ErrTxExpired, tx)
|
|
})
|
|
t.Run("BlockedAccount", func(t *testing.T) {
|
|
tx := bc.newTestTx(accs[1].PrivateKey().GetScriptHash(), testScript)
|
|
require.NoError(t, accs[1].SignTx(netmode.UnitTestNet, tx))
|
|
err := bc.VerifyTx(tx)
|
|
require.True(t, errors.Is(err, ErrPolicy))
|
|
})
|
|
t.Run("InsufficientGas", func(t *testing.T) {
|
|
balance := bc.GetUtilityTokenBalance(h)
|
|
tx := bc.newTestTx(h, testScript)
|
|
tx.SystemFee = balance.Int64() + 1
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
|
|
checkErr(t, ErrInsufficientFunds, tx)
|
|
})
|
|
t.Run("TooBigTx", func(t *testing.T) {
|
|
script := make([]byte, transaction.MaxTransactionSize)
|
|
tx := bc.newTestTx(h, script)
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
|
|
checkErr(t, ErrTxTooBig, tx)
|
|
})
|
|
t.Run("NetworkFee", func(t *testing.T) {
|
|
t.Run("SmallNetworkFee", func(t *testing.T) {
|
|
tx := bc.newTestTx(h, testScript)
|
|
tx.NetworkFee = 1
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
|
|
checkErr(t, ErrTxSmallNetworkFee, tx)
|
|
})
|
|
t.Run("AlmostEnoughNetworkFee", func(t *testing.T) {
|
|
tx := bc.newTestTx(h, testScript)
|
|
verificationNetFee, calcultedScriptSize := fee.Calculate(bc.GetBaseExecFee(), accs[0].Contract.Script)
|
|
expectedSize := io.GetVarSize(tx) + calcultedScriptSize
|
|
calculatedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte()
|
|
tx.NetworkFee = calculatedNetFee - 1
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
|
|
require.Equal(t, expectedSize, io.GetVarSize(tx))
|
|
checkErr(t, ErrVerificationFailed, tx)
|
|
})
|
|
t.Run("EnoughNetworkFee", func(t *testing.T) {
|
|
tx := bc.newTestTx(h, testScript)
|
|
verificationNetFee, calcultedScriptSize := fee.Calculate(bc.GetBaseExecFee(), accs[0].Contract.Script)
|
|
expectedSize := io.GetVarSize(tx) + calcultedScriptSize
|
|
calculatedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte()
|
|
tx.NetworkFee = calculatedNetFee
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
|
|
require.Equal(t, expectedSize, io.GetVarSize(tx))
|
|
require.NoError(t, bc.VerifyTx(tx))
|
|
})
|
|
t.Run("CalculateNetworkFee, signature script", func(t *testing.T) {
|
|
tx := bc.newTestTx(h, testScript)
|
|
expectedSize := io.GetVarSize(tx)
|
|
verificationNetFee, calculatedScriptSize := fee.Calculate(bc.GetBaseExecFee(), accs[0].Contract.Script)
|
|
expectedSize += calculatedScriptSize
|
|
expectedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte()
|
|
tx.NetworkFee = expectedNetFee
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
|
|
actualSize := io.GetVarSize(tx)
|
|
require.Equal(t, expectedSize, actualSize)
|
|
interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, nil, tx)
|
|
gasConsumed, err := bc.verifyHashAgainstScript(h, &tx.Scripts[0], interopCtx, -1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, verificationNetFee, gasConsumed)
|
|
require.Equal(t, expectedNetFee, bc.FeePerByte()*int64(actualSize)+gasConsumed)
|
|
})
|
|
t.Run("CalculateNetworkFee, multisignature script", func(t *testing.T) {
|
|
multisigAcc := accs[4]
|
|
pKeys := keys.PublicKeys{multisigAcc.PrivateKey().PublicKey()}
|
|
require.NoError(t, multisigAcc.ConvertMultisig(1, pKeys))
|
|
multisigHash := hash.Hash160(multisigAcc.Contract.Script)
|
|
tx := bc.newTestTx(multisigHash, testScript)
|
|
verificationNetFee, calculatedScriptSize := fee.Calculate(bc.GetBaseExecFee(), multisigAcc.Contract.Script)
|
|
expectedSize := io.GetVarSize(tx) + calculatedScriptSize
|
|
expectedNetFee := verificationNetFee + int64(expectedSize)*bc.FeePerByte()
|
|
tx.NetworkFee = expectedNetFee
|
|
require.NoError(t, multisigAcc.SignTx(netmode.UnitTestNet, tx))
|
|
actualSize := io.GetVarSize(tx)
|
|
require.Equal(t, expectedSize, actualSize)
|
|
interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, nil, tx)
|
|
gasConsumed, err := bc.verifyHashAgainstScript(multisigHash, &tx.Scripts[0], interopCtx, -1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, verificationNetFee, gasConsumed)
|
|
require.Equal(t, expectedNetFee, bc.FeePerByte()*int64(actualSize)+gasConsumed)
|
|
})
|
|
})
|
|
t.Run("InvalidTxScript", func(t *testing.T) {
|
|
tx := bc.newTestTx(h, testScript)
|
|
tx.Script = append(tx.Script, 0xff)
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
|
|
checkErr(t, ErrInvalidScript, tx)
|
|
})
|
|
t.Run("InvalidVerificationScript", func(t *testing.T) {
|
|
tx := bc.newTestTx(h, testScript)
|
|
verif := []byte{byte(opcode.JMP), 3, 0xff, byte(opcode.PUSHT)}
|
|
tx.Signers = append(tx.Signers, transaction.Signer{
|
|
Account: hash.Hash160(verif),
|
|
Scopes: transaction.Global,
|
|
})
|
|
tx.NetworkFee += 1000000
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
|
|
tx.Scripts = append(tx.Scripts, transaction.Witness{
|
|
InvocationScript: []byte{},
|
|
VerificationScript: verif,
|
|
})
|
|
checkErr(t, ErrInvalidVerification, tx)
|
|
})
|
|
t.Run("InvalidInvocationScript", func(t *testing.T) {
|
|
tx := bc.newTestTx(h, testScript)
|
|
verif := []byte{byte(opcode.PUSHT)}
|
|
tx.Signers = append(tx.Signers, transaction.Signer{
|
|
Account: hash.Hash160(verif),
|
|
Scopes: transaction.Global,
|
|
})
|
|
tx.NetworkFee += 1000000
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
|
|
tx.Scripts = append(tx.Scripts, transaction.Witness{
|
|
InvocationScript: []byte{byte(opcode.JMP), 3, 0xff},
|
|
VerificationScript: verif,
|
|
})
|
|
checkErr(t, ErrInvalidInvocation, tx)
|
|
})
|
|
t.Run("Conflict", func(t *testing.T) {
|
|
balance := bc.GetUtilityTokenBalance(h).Int64()
|
|
tx := bc.newTestTx(h, testScript)
|
|
tx.NetworkFee = balance / 2
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
|
|
require.NoError(t, bc.PoolTx(tx))
|
|
|
|
tx2 := bc.newTestTx(h, testScript)
|
|
tx2.NetworkFee = balance / 2
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx2))
|
|
err := bc.PoolTx(tx2)
|
|
require.True(t, errors.Is(err, ErrMemPoolConflict))
|
|
})
|
|
t.Run("InvalidWitnessHash", func(t *testing.T) {
|
|
tx := bc.newTestTx(h, testScript)
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
|
|
tx.Scripts[0].VerificationScript = []byte{byte(opcode.PUSHT)}
|
|
checkErr(t, ErrWitnessHashMismatch, tx)
|
|
})
|
|
t.Run("InvalidWitnessSignature", func(t *testing.T) {
|
|
tx := bc.newTestTx(h, testScript)
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
|
|
tx.Scripts[0].InvocationScript[10] = ^tx.Scripts[0].InvocationScript[10]
|
|
checkErr(t, ErrVerificationFailed, tx)
|
|
})
|
|
t.Run("InsufficientNetworkFeeForSecondWitness", func(t *testing.T) {
|
|
tx := bc.newTestTx(h, testScript)
|
|
tx.Signers = append(tx.Signers, transaction.Signer{
|
|
Account: accs[3].PrivateKey().GetScriptHash(),
|
|
Scopes: transaction.Global,
|
|
})
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
|
|
require.NoError(t, accs[3].SignTx(netmode.UnitTestNet, tx))
|
|
checkErr(t, ErrVerificationFailed, tx)
|
|
})
|
|
t.Run("OldTX", func(t *testing.T) {
|
|
tx := bc.newTestTx(h, testScript)
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
|
|
b := bc.newBlock(tx)
|
|
require.NoError(t, bc.AddBlock(b))
|
|
|
|
err := bc.VerifyTx(tx)
|
|
require.True(t, errors.Is(err, ErrAlreadyExists))
|
|
})
|
|
t.Run("MemPooledTX", func(t *testing.T) {
|
|
tx := bc.newTestTx(h, testScript)
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
|
|
require.NoError(t, bc.PoolTx(tx))
|
|
|
|
err := bc.PoolTx(tx)
|
|
require.True(t, errors.Is(err, ErrAlreadyExists))
|
|
})
|
|
t.Run("MemPoolOOM", func(t *testing.T) {
|
|
bc.memPool = mempool.New(1, 0, false)
|
|
tx1 := bc.newTestTx(h, testScript)
|
|
tx1.NetworkFee += 10000 // Give it more priority.
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx1))
|
|
require.NoError(t, bc.PoolTx(tx1))
|
|
|
|
tx2 := bc.newTestTx(h, testScript)
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx2))
|
|
err := bc.PoolTx(tx2)
|
|
require.True(t, errors.Is(err, ErrOOM))
|
|
})
|
|
t.Run("Attribute", func(t *testing.T) {
|
|
t.Run("InvalidHighPriority", func(t *testing.T) {
|
|
tx := bc.newTestTx(h, testScript)
|
|
tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.HighPriority})
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
|
|
checkErr(t, ErrInvalidAttribute, tx)
|
|
})
|
|
t.Run("ValidHighPriority", func(t *testing.T) {
|
|
tx := bc.newTestTx(h, testScript)
|
|
tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.HighPriority})
|
|
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(bc.GetBaseExecFee(), rawScript)
|
|
tx.NetworkFee += netFee
|
|
tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte()
|
|
tx.Scripts = []transaction.Witness{{
|
|
InvocationScript: testchain.SignCommittee(tx),
|
|
VerificationScript: rawScript,
|
|
}}
|
|
require.NoError(t, bc.VerifyTx(tx))
|
|
})
|
|
t.Run("Oracle", func(t *testing.T) {
|
|
orc := bc.contracts.Oracle
|
|
req := &state.OracleRequest{GasForResponse: 1000_0000}
|
|
require.NoError(t, orc.PutRequestInternal(1, req, bc.dao))
|
|
|
|
oracleScript, err := smartcontract.CreateMajorityMultiSigRedeemScript(oraclePubs)
|
|
require.NoError(t, err)
|
|
oracleHash := hash.Hash160(oracleScript)
|
|
|
|
// We need to create new transaction,
|
|
// because hashes are cached after signing.
|
|
getOracleTx := func(t *testing.T) *transaction.Transaction {
|
|
tx := bc.newTestTx(h, orc.GetOracleResponseScript())
|
|
resp := &transaction.OracleResponse{
|
|
ID: 1,
|
|
Code: transaction.Success,
|
|
Result: []byte{1, 2, 3},
|
|
}
|
|
tx.Attributes = []transaction.Attribute{{
|
|
Type: transaction.OracleResponseT,
|
|
Value: resp,
|
|
}}
|
|
tx.NetworkFee += 4_000_000 // multisig check
|
|
tx.SystemFee = int64(req.GasForResponse - uint64(tx.NetworkFee))
|
|
tx.Signers = []transaction.Signer{{
|
|
Account: oracleHash,
|
|
Scopes: transaction.None,
|
|
}}
|
|
size := io.GetVarSize(tx)
|
|
netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), oracleScript)
|
|
tx.NetworkFee += netFee
|
|
tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte()
|
|
return tx
|
|
}
|
|
|
|
t.Run("NoOracleNodes", func(t *testing.T) {
|
|
tx := getOracleTx(t)
|
|
require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx))
|
|
checkErr(t, ErrInvalidAttribute, tx)
|
|
})
|
|
|
|
txSetOracle := transaction.New([]byte{byte(opcode.RET)}, 0) // it's a hack, so we don't need a real script
|
|
setSigner(txSetOracle, testchain.CommitteeScriptHash())
|
|
txSetOracle.Scripts = []transaction.Witness{{
|
|
InvocationScript: testchain.SignCommittee(txSetOracle),
|
|
VerificationScript: testchain.CommitteeVerificationScript(),
|
|
}}
|
|
bl := block.New(bc.config.StateRootInHeader)
|
|
bl.Index = bc.BlockHeight() + 1
|
|
ic := bc.newInteropContext(trigger.All, bc.dao, bl, txSetOracle)
|
|
ic.SpawnVM()
|
|
ic.VM.LoadScript([]byte{byte(opcode.RET)})
|
|
require.NoError(t, bc.contracts.Designate.DesignateAsRole(ic, noderoles.Oracle, oraclePubs))
|
|
_, err = ic.DAO.Persist()
|
|
require.NoError(t, err)
|
|
|
|
t.Run("Valid", func(t *testing.T) {
|
|
tx := getOracleTx(t)
|
|
require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx))
|
|
require.NoError(t, bc.VerifyTx(tx))
|
|
|
|
t.Run("NativeVerify", func(t *testing.T) {
|
|
tx.Signers = append(tx.Signers, transaction.Signer{
|
|
Account: bc.contracts.Oracle.Hash,
|
|
Scopes: transaction.None,
|
|
})
|
|
tx.Scripts = append(tx.Scripts, transaction.Witness{})
|
|
t.Run("NonZeroVerification", func(t *testing.T) {
|
|
w := io.NewBufBinWriter()
|
|
emit.Opcodes(w.BinWriter, opcode.ABORT)
|
|
emit.Bytes(w.BinWriter, util.Uint160{}.BytesBE())
|
|
emit.Int(w.BinWriter, 0)
|
|
emit.String(w.BinWriter, orc.Manifest.Name)
|
|
tx.Scripts[len(tx.Scripts)-1].VerificationScript = w.Bytes()
|
|
err := bc.VerifyTx(tx)
|
|
require.True(t, errors.Is(err, ErrNativeContractWitness), "got: %v", err)
|
|
})
|
|
t.Run("Good", func(t *testing.T) {
|
|
tx.Scripts[len(tx.Scripts)-1].VerificationScript = nil
|
|
require.NoError(t, bc.VerifyTx(tx))
|
|
})
|
|
})
|
|
})
|
|
t.Run("InvalidRequestID", func(t *testing.T) {
|
|
tx := getOracleTx(t)
|
|
tx.Attributes[0].Value.(*transaction.OracleResponse).ID = 2
|
|
require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx))
|
|
checkErr(t, ErrInvalidAttribute, tx)
|
|
})
|
|
t.Run("InvalidScope", func(t *testing.T) {
|
|
tx := getOracleTx(t)
|
|
tx.Signers[0].Scopes = transaction.Global
|
|
require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx))
|
|
checkErr(t, ErrInvalidAttribute, tx)
|
|
})
|
|
t.Run("InvalidScript", func(t *testing.T) {
|
|
tx := getOracleTx(t)
|
|
tx.Script = append(tx.Script, byte(opcode.NOP))
|
|
require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx))
|
|
checkErr(t, ErrInvalidAttribute, tx)
|
|
})
|
|
t.Run("InvalidSigner", func(t *testing.T) {
|
|
tx := getOracleTx(t)
|
|
tx.Signers[0].Account = accs[0].Contract.ScriptHash()
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
|
|
checkErr(t, ErrInvalidAttribute, tx)
|
|
})
|
|
t.Run("SmallFee", func(t *testing.T) {
|
|
tx := getOracleTx(t)
|
|
tx.SystemFee = 0
|
|
require.NoError(t, oracleAcc.SignTx(netmode.UnitTestNet, tx))
|
|
checkErr(t, ErrInvalidAttribute, tx)
|
|
})
|
|
})
|
|
t.Run("NotValidBefore", func(t *testing.T) {
|
|
getNVBTx := func(height uint32) *transaction.Transaction {
|
|
tx := bc.newTestTx(h, testScript)
|
|
tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: height}})
|
|
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(bc.GetBaseExecFee(), rawScript)
|
|
tx.NetworkFee += netFee
|
|
tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte()
|
|
tx.Scripts = []transaction.Witness{{
|
|
InvocationScript: testchain.SignCommittee(tx),
|
|
VerificationScript: rawScript,
|
|
}}
|
|
return tx
|
|
}
|
|
t.Run("Disabled", func(t *testing.T) {
|
|
tx := getNVBTx(bc.blockHeight + 1)
|
|
require.Error(t, bc.VerifyTx(tx))
|
|
})
|
|
t.Run("Enabled", func(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), ErrInvalidAttribute))
|
|
})
|
|
t.Run("positive", func(t *testing.T) {
|
|
tx := getNVBTx(bc.blockHeight)
|
|
require.NoError(t, bc.VerifyTx(tx))
|
|
})
|
|
})
|
|
})
|
|
t.Run("Reserved", func(t *testing.T) {
|
|
getReservedTx := func(attrType transaction.AttrType) *transaction.Transaction {
|
|
tx := bc.newTestTx(h, testScript)
|
|
tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: attrType, Value: &transaction.Reserved{Value: []byte{1, 2, 3}}})
|
|
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(bc.GetBaseExecFee(), rawScript)
|
|
tx.NetworkFee += netFee
|
|
tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte()
|
|
tx.Scripts = []transaction.Witness{{
|
|
InvocationScript: testchain.SignCommittee(tx),
|
|
VerificationScript: rawScript,
|
|
}}
|
|
return tx
|
|
}
|
|
t.Run("Disabled", func(t *testing.T) {
|
|
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 + 3)
|
|
require.NoError(t, bc.VerifyTx(tx))
|
|
})
|
|
})
|
|
t.Run("Conflicts", func(t *testing.T) {
|
|
getConflictsTx := func(hashes ...util.Uint256) *transaction.Transaction {
|
|
tx := bc.newTestTx(h, testScript)
|
|
tx.Attributes = make([]transaction.Attribute, len(hashes))
|
|
for i, h := range hashes {
|
|
tx.Attributes[i] = transaction.Attribute{
|
|
Type: transaction.ConflictsT,
|
|
Value: &transaction.Conflicts{
|
|
Hash: h,
|
|
},
|
|
}
|
|
}
|
|
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(bc.GetBaseExecFee(), rawScript)
|
|
tx.NetworkFee += netFee
|
|
tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte()
|
|
tx.Scripts = []transaction.Witness{{
|
|
InvocationScript: testchain.SignCommittee(tx),
|
|
VerificationScript: rawScript,
|
|
}}
|
|
return tx
|
|
}
|
|
t.Run("disabled", func(t *testing.T) {
|
|
bc.config.P2PSigExtensions = false
|
|
tx := getConflictsTx(util.Uint256{1, 2, 3})
|
|
require.Error(t, bc.VerifyTx(tx))
|
|
})
|
|
t.Run("enabled", func(t *testing.T) {
|
|
bc.config.P2PSigExtensions = true
|
|
t.Run("dummy on-chain conflict", func(t *testing.T) {
|
|
tx := bc.newTestTx(h, testScript)
|
|
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
|
|
conflicting := transaction.New([]byte{byte(opcode.RET)}, 1)
|
|
conflicting.Attributes = []transaction.Attribute{
|
|
{
|
|
Type: transaction.ConflictsT,
|
|
Value: &transaction.Conflicts{
|
|
Hash: tx.Hash(),
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, bc.dao.StoreAsTransaction(conflicting, bc.blockHeight, nil, nil))
|
|
require.True(t, errors.Is(bc.VerifyTx(tx), ErrHasConflicts))
|
|
})
|
|
t.Run("attribute on-chain conflict", func(t *testing.T) {
|
|
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx.ValidUntilBlock = 4242
|
|
tx.Signers = []transaction.Signer{{
|
|
Account: testchain.MultisigScriptHash(),
|
|
Scopes: transaction.None,
|
|
}}
|
|
require.NoError(t, testchain.SignTx(bc, tx))
|
|
b := bc.newBlock(tx)
|
|
|
|
require.NoError(t, bc.AddBlock(b))
|
|
txConflict := getConflictsTx(tx.Hash())
|
|
require.Error(t, bc.VerifyTx(txConflict))
|
|
})
|
|
t.Run("positive", func(t *testing.T) {
|
|
tx := getConflictsTx(random.Uint256())
|
|
require.NoError(t, bc.VerifyTx(tx))
|
|
})
|
|
})
|
|
})
|
|
t.Run("NotaryAssisted", func(t *testing.T) {
|
|
notary, err := wallet.NewAccount()
|
|
require.NoError(t, err)
|
|
txSetNotary := transaction.New([]byte{byte(opcode.RET)}, 0)
|
|
setSigner(txSetNotary, testchain.CommitteeScriptHash())
|
|
txSetNotary.Scripts = []transaction.Witness{{
|
|
InvocationScript: testchain.SignCommittee(txSetNotary),
|
|
VerificationScript: testchain.CommitteeVerificationScript(),
|
|
}}
|
|
bl := block.New(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, noderoles.P2PNotary, keys.PublicKeys{notary.PrivateKey().PublicKey()}))
|
|
_, 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{
|
|
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,
|
|
},
|
|
{
|
|
Account: bc.contracts.Notary.Hash,
|
|
Scopes: transaction.None,
|
|
},
|
|
}
|
|
rawScript := testchain.CommitteeVerificationScript()
|
|
size := io.GetVarSize(tx)
|
|
netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), rawScript)
|
|
tx.NetworkFee += netFee
|
|
tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte()
|
|
tx.Scripts = []transaction.Witness{
|
|
{
|
|
InvocationScript: testchain.SignCommittee(tx),
|
|
VerificationScript: rawScript,
|
|
},
|
|
{
|
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(testchain.Network()), tx)...),
|
|
},
|
|
}
|
|
return tx
|
|
}
|
|
t.Run("Disabled", func(t *testing.T) {
|
|
bc.config.P2PSigExtensions = false
|
|
tx := getNotaryAssistedTx(0, 0)
|
|
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("Test verify", func(t *testing.T) {
|
|
bc.config.P2PSigExtensions = true
|
|
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,
|
|
},
|
|
}
|
|
tx.Scripts = []transaction.Witness{
|
|
{
|
|
InvocationScript: testchain.SignCommittee(tx),
|
|
VerificationScript: testchain.CommitteeVerificationScript(),
|
|
},
|
|
{
|
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(testchain.Network()), tx)...),
|
|
},
|
|
}
|
|
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,
|
|
},
|
|
}
|
|
tx.Scripts = []transaction.Witness{
|
|
{
|
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(testchain.Network()), tx)...),
|
|
},
|
|
{
|
|
InvocationScript: testchain.SignCommittee(tx),
|
|
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,
|
|
},
|
|
}
|
|
tx.Scripts = []transaction.Witness{
|
|
{
|
|
InvocationScript: testchain.SignCommittee(tx),
|
|
VerificationScript: testchain.CommitteeVerificationScript(),
|
|
},
|
|
{
|
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(testchain.Network()), tx)...),
|
|
},
|
|
}
|
|
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,
|
|
},
|
|
}
|
|
tx.Scripts = []transaction.Witness{
|
|
{
|
|
InvocationScript: testchain.SignCommittee(tx),
|
|
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,
|
|
},
|
|
}
|
|
acc, err := keys.NewPrivateKey()
|
|
require.NoError(t, err)
|
|
tx.Scripts = []transaction.Witness{
|
|
{
|
|
InvocationScript: testchain.SignCommittee(tx),
|
|
VerificationScript: testchain.CommitteeVerificationScript(),
|
|
},
|
|
{
|
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc.SignHashable(uint32(testchain.Network()), tx)...),
|
|
},
|
|
}
|
|
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,
|
|
},
|
|
}
|
|
tx.Scripts = []transaction.Witness{
|
|
{
|
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().SignHashable(uint32(testchain.Network()), tx)...),
|
|
},
|
|
}
|
|
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))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
t.Run("Partially-filled transaction", func(t *testing.T) {
|
|
bc.config.P2PSigExtensions = true
|
|
getPartiallyFilledTx := func(nvb uint32, validUntil uint32) *transaction.Transaction {
|
|
tx := bc.newTestTx(h, testScript)
|
|
tx.ValidUntilBlock = validUntil
|
|
tx.Attributes = []transaction.Attribute{
|
|
{
|
|
Type: transaction.NotValidBeforeT,
|
|
Value: &transaction.NotValidBefore{Height: nvb},
|
|
},
|
|
{
|
|
Type: transaction.NotaryAssistedT,
|
|
Value: &transaction.NotaryAssisted{NKeys: 0},
|
|
},
|
|
}
|
|
tx.Signers = []transaction.Signer{
|
|
{
|
|
Account: bc.contracts.Notary.Hash,
|
|
Scopes: transaction.None,
|
|
},
|
|
{
|
|
Account: testchain.MultisigScriptHash(),
|
|
Scopes: transaction.None,
|
|
},
|
|
}
|
|
size := io.GetVarSize(tx)
|
|
netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), testchain.MultisigVerificationScript())
|
|
tx.NetworkFee = netFee + // multisig witness verification price
|
|
int64(size)*bc.FeePerByte() + // fee for unsigned size
|
|
int64(sizeDelta)*bc.FeePerByte() + //fee for multisig size
|
|
66*bc.FeePerByte() + // fee for Notary signature size (66 bytes for Invocation script and 0 bytes for Verification script)
|
|
2*bc.FeePerByte() + // fee for the length of each script in Notary witness (they are nil, so we did not take them into account during `size` calculation)
|
|
transaction.NotaryServiceFeePerKey + // fee for Notary attribute
|
|
fee.Opcode(bc.GetBaseExecFee(), // Notary verification script
|
|
opcode.PUSHDATA1, opcode.RET, // invocation script
|
|
opcode.PUSH0, opcode.SYSCALL, opcode.RET) + // Neo.Native.Call
|
|
nativeprices.NotaryVerificationPrice*bc.GetBaseExecFee() // Notary witness verification price
|
|
tx.Scripts = []transaction.Witness{
|
|
{
|
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64)...),
|
|
VerificationScript: []byte{},
|
|
},
|
|
{
|
|
InvocationScript: testchain.Sign(tx),
|
|
VerificationScript: testchain.MultisigVerificationScript(),
|
|
},
|
|
}
|
|
return tx
|
|
}
|
|
|
|
mp := mempool.New(10, 1, false)
|
|
verificationF := func(bc blockchainer.Blockchainer, tx *transaction.Transaction, data interface{}) error {
|
|
if data.(int) > 5 {
|
|
return errors.New("bad data")
|
|
}
|
|
return nil
|
|
}
|
|
t.Run("failed pre-verification", func(t *testing.T) {
|
|
tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+1)
|
|
require.Error(t, bc.PoolTxWithData(tx, 6, mp, bc, verificationF)) // here and below let's use `bc` instead of proper NotaryFeer for the test simplicity.
|
|
})
|
|
t.Run("GasLimitExceeded during witness verification", func(t *testing.T) {
|
|
tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+1)
|
|
tx.NetworkFee-- // to check that NetworkFee was set correctly in getPartiallyFilledTx
|
|
tx.Scripts = []transaction.Witness{
|
|
{
|
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64)...),
|
|
VerificationScript: []byte{},
|
|
},
|
|
{
|
|
InvocationScript: testchain.Sign(tx),
|
|
VerificationScript: testchain.MultisigVerificationScript(),
|
|
},
|
|
}
|
|
require.Error(t, bc.PoolTxWithData(tx, 5, mp, bc, verificationF))
|
|
})
|
|
t.Run("bad NVB: too big", func(t *testing.T) {
|
|
tx := getPartiallyFilledTx(bc.blockHeight+bc.contracts.Notary.GetMaxNotValidBeforeDelta(bc.dao)+1, bc.blockHeight+1)
|
|
require.True(t, errors.Is(bc.PoolTxWithData(tx, 5, mp, bc, verificationF), ErrInvalidAttribute))
|
|
})
|
|
t.Run("bad ValidUntilBlock: too small", func(t *testing.T) {
|
|
tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+bc.contracts.Notary.GetMaxNotValidBeforeDelta(bc.dao)+1)
|
|
require.True(t, errors.Is(bc.PoolTxWithData(tx, 5, mp, bc, verificationF), ErrInvalidAttribute))
|
|
})
|
|
t.Run("good", func(t *testing.T) {
|
|
tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+1)
|
|
require.NoError(t, bc.PoolTxWithData(tx, 5, mp, bc, verificationF))
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestVerifyHashAgainstScript(t *testing.T) {
|
|
bc := newTestChain(t)
|
|
|
|
cs, csInvalid := getTestContractState(bc)
|
|
ic := bc.newInteropContext(trigger.Verification, bc.dao, nil, nil)
|
|
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs))
|
|
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, csInvalid))
|
|
|
|
gas := bc.contracts.Policy.GetMaxVerificationGas(ic.DAO)
|
|
t.Run("Contract", func(t *testing.T) {
|
|
t.Run("Missing", func(t *testing.T) {
|
|
newH := cs.Hash
|
|
newH[0] = ^newH[0]
|
|
w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH4)}}
|
|
_, err := bc.verifyHashAgainstScript(newH, w, ic, gas)
|
|
require.True(t, errors.Is(err, ErrUnknownVerificationContract))
|
|
})
|
|
t.Run("Invalid", func(t *testing.T) {
|
|
w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH4)}}
|
|
_, err := bc.verifyHashAgainstScript(csInvalid.Hash, w, ic, gas)
|
|
require.True(t, errors.Is(err, ErrInvalidVerificationContract))
|
|
})
|
|
t.Run("ValidSignature", func(t *testing.T) {
|
|
w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH4)}}
|
|
_, err := bc.verifyHashAgainstScript(cs.Hash, w, ic, gas)
|
|
require.NoError(t, err)
|
|
})
|
|
t.Run("InvalidSignature", func(t *testing.T) {
|
|
w := &transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH3)}}
|
|
_, err := bc.verifyHashAgainstScript(cs.Hash, w, ic, gas)
|
|
require.True(t, errors.Is(err, ErrVerificationFailed))
|
|
})
|
|
})
|
|
t.Run("NotEnoughGas", func(t *testing.T) {
|
|
verif := []byte{byte(opcode.PUSH1)}
|
|
w := &transaction.Witness{
|
|
InvocationScript: []byte{byte(opcode.NOP)},
|
|
VerificationScript: verif,
|
|
}
|
|
_, err := bc.verifyHashAgainstScript(hash.Hash160(verif), w, ic, 1)
|
|
require.True(t, errors.Is(err, ErrVerificationFailed))
|
|
})
|
|
t.Run("NoResult", func(t *testing.T) {
|
|
verif := []byte{byte(opcode.DROP)}
|
|
w := &transaction.Witness{
|
|
InvocationScript: []byte{byte(opcode.PUSH1)},
|
|
VerificationScript: verif,
|
|
}
|
|
_, err := bc.verifyHashAgainstScript(hash.Hash160(verif), w, ic, gas)
|
|
require.True(t, errors.Is(err, ErrVerificationFailed))
|
|
})
|
|
t.Run("BadResult", func(t *testing.T) {
|
|
verif := make([]byte, 66)
|
|
verif[0] = byte(opcode.PUSHDATA1)
|
|
verif[1] = 64
|
|
w := &transaction.Witness{
|
|
InvocationScript: []byte{byte(opcode.NOP)},
|
|
VerificationScript: verif,
|
|
}
|
|
_, err := bc.verifyHashAgainstScript(hash.Hash160(verif), w, ic, gas)
|
|
require.True(t, errors.Is(err, ErrVerificationFailed))
|
|
})
|
|
t.Run("TooManyResults", func(t *testing.T) {
|
|
verif := []byte{byte(opcode.NOP)}
|
|
w := &transaction.Witness{
|
|
InvocationScript: []byte{byte(opcode.PUSH1), byte(opcode.PUSH1)},
|
|
VerificationScript: verif,
|
|
}
|
|
_, err := bc.verifyHashAgainstScript(hash.Hash160(verif), w, ic, gas)
|
|
require.True(t, errors.Is(err, ErrVerificationFailed))
|
|
})
|
|
}
|
|
|
|
func TestIsTxStillRelevant(t *testing.T) {
|
|
bc := newTestChain(t)
|
|
|
|
mp := bc.GetMemPool()
|
|
newTx := func(t *testing.T) *transaction.Transaction {
|
|
tx := transaction.New([]byte{byte(opcode.RET)}, 100)
|
|
tx.ValidUntilBlock = bc.BlockHeight() + 1
|
|
tx.Signers = []transaction.Signer{{
|
|
Account: neoOwner,
|
|
Scopes: transaction.CalledByEntry,
|
|
}}
|
|
return tx
|
|
}
|
|
|
|
t.Run("small ValidUntilBlock", func(t *testing.T) {
|
|
tx := newTx(t)
|
|
require.NoError(t, testchain.SignTx(bc, tx))
|
|
|
|
require.True(t, bc.IsTxStillRelevant(tx, nil, false))
|
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
|
require.False(t, bc.IsTxStillRelevant(tx, nil, false))
|
|
})
|
|
|
|
t.Run("tx is already persisted", func(t *testing.T) {
|
|
tx := newTx(t)
|
|
tx.ValidUntilBlock = bc.BlockHeight() + 2
|
|
require.NoError(t, testchain.SignTx(bc, tx))
|
|
|
|
require.True(t, bc.IsTxStillRelevant(tx, nil, false))
|
|
require.NoError(t, bc.AddBlock(bc.newBlock(tx)))
|
|
require.False(t, bc.IsTxStillRelevant(tx, nil, false))
|
|
})
|
|
|
|
t.Run("tx with Conflicts attribute", func(t *testing.T) {
|
|
tx1 := newTx(t)
|
|
require.NoError(t, testchain.SignTx(bc, tx1))
|
|
|
|
tx2 := newTx(t)
|
|
tx2.Attributes = []transaction.Attribute{{
|
|
Type: transaction.ConflictsT,
|
|
Value: &transaction.Conflicts{Hash: tx1.Hash()},
|
|
}}
|
|
require.NoError(t, testchain.SignTx(bc, tx2))
|
|
|
|
require.True(t, bc.IsTxStillRelevant(tx1, mp, false))
|
|
require.NoError(t, bc.verifyAndPoolTx(tx2, mp, bc))
|
|
require.False(t, bc.IsTxStillRelevant(tx1, mp, false))
|
|
})
|
|
t.Run("NotValidBefore", func(t *testing.T) {
|
|
tx3 := newTx(t)
|
|
tx3.Attributes = []transaction.Attribute{{
|
|
Type: transaction.NotValidBeforeT,
|
|
Value: &transaction.NotValidBefore{Height: bc.BlockHeight() + 1},
|
|
}}
|
|
tx3.ValidUntilBlock = bc.BlockHeight() + 2
|
|
require.NoError(t, testchain.SignTx(bc, tx3))
|
|
|
|
require.False(t, bc.IsTxStillRelevant(tx3, nil, false))
|
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
|
require.True(t, bc.IsTxStillRelevant(tx3, nil, false))
|
|
})
|
|
t.Run("contract witness check fails", func(t *testing.T) {
|
|
src := fmt.Sprintf(`package verify
|
|
import (
|
|
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
|
|
"github.com/nspcc-dev/neo-go/pkg/interop/util"
|
|
)
|
|
func Verify() bool {
|
|
addr := util.FromAddress("`+address.Uint160ToString(bc.contracts.Ledger.Hash)+`")
|
|
currentHeight := contract.Call(addr, "currentIndex", contract.ReadStates)
|
|
return currentHeight.(int) < %d
|
|
}`, bc.BlockHeight()+2) // deploy + next block
|
|
txDeploy, h, _, err := testchain.NewDeployTx(bc, "TestVerify", neoOwner, strings.NewReader(src), nil)
|
|
require.NoError(t, err)
|
|
txDeploy.ValidUntilBlock = bc.BlockHeight() + 1
|
|
addSigners(neoOwner, txDeploy)
|
|
require.NoError(t, testchain.SignTx(bc, txDeploy))
|
|
require.NoError(t, bc.AddBlock(bc.newBlock(txDeploy)))
|
|
|
|
tx := newTx(t)
|
|
tx.Signers = append(tx.Signers, transaction.Signer{
|
|
Account: h,
|
|
Scopes: transaction.None,
|
|
})
|
|
tx.NetworkFee += 10_000_000
|
|
require.NoError(t, testchain.SignTx(bc, tx))
|
|
tx.Scripts = append(tx.Scripts, transaction.Witness{})
|
|
|
|
require.True(t, bc.IsTxStillRelevant(tx, mp, false))
|
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
|
require.False(t, bc.IsTxStillRelevant(tx, mp, false))
|
|
})
|
|
}
|
|
|
|
func TestMemPoolRemoval(t *testing.T) {
|
|
const added = 16
|
|
const notAdded = 32
|
|
bc := newTestChain(t)
|
|
addedTxes := make([]*transaction.Transaction, added)
|
|
notAddedTxes := make([]*transaction.Transaction, notAdded)
|
|
for i := range addedTxes {
|
|
addedTxes[i] = bc.newTestTx(testchain.MultisigScriptHash(), []byte{byte(opcode.PUSH1)})
|
|
require.NoError(t, testchain.SignTx(bc, addedTxes[i]))
|
|
require.NoError(t, bc.PoolTx(addedTxes[i]))
|
|
}
|
|
for i := range notAddedTxes {
|
|
notAddedTxes[i] = bc.newTestTx(testchain.MultisigScriptHash(), []byte{byte(opcode.PUSH1)})
|
|
require.NoError(t, testchain.SignTx(bc, notAddedTxes[i]))
|
|
require.NoError(t, bc.PoolTx(notAddedTxes[i]))
|
|
}
|
|
b := bc.newBlock(addedTxes...)
|
|
require.NoError(t, bc.AddBlock(b))
|
|
mempool := bc.GetMemPool()
|
|
for _, tx := range addedTxes {
|
|
require.False(t, mempool.ContainsKey(tx.Hash()))
|
|
}
|
|
for _, tx := range notAddedTxes {
|
|
require.True(t, mempool.ContainsKey(tx.Hash()))
|
|
}
|
|
}
|
|
|
|
func TestHasBlock(t *testing.T) {
|
|
bc := newTestChain(t)
|
|
blocks, err := bc.genBlocks(50)
|
|
require.NoError(t, err)
|
|
|
|
// Test unpersisted and persisted access
|
|
for j := 0; j < 2; j++ {
|
|
for i := 0; i < len(blocks); i++ {
|
|
assert.True(t, bc.HasBlock(blocks[i].Hash()))
|
|
}
|
|
newBlock := bc.newBlock()
|
|
assert.False(t, bc.HasBlock(newBlock.Hash()))
|
|
_, err = bc.persist(true)
|
|
assert.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
func TestGetTransaction(t *testing.T) {
|
|
bc := newTestChain(t)
|
|
tx1 := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
|
|
tx1.ValidUntilBlock = 16
|
|
tx1.Signers = []transaction.Signer{{
|
|
Account: testchain.MultisigScriptHash(),
|
|
Scopes: transaction.CalledByEntry,
|
|
}}
|
|
tx2 := transaction.New([]byte{byte(opcode.PUSH2)}, 0)
|
|
tx2.ValidUntilBlock = 16
|
|
tx2.Signers = []transaction.Signer{{
|
|
Account: testchain.MultisigScriptHash(),
|
|
Scopes: transaction.CalledByEntry,
|
|
}}
|
|
require.NoError(t, testchain.SignTx(bc, tx1, tx2))
|
|
b1 := bc.newBlock(tx1)
|
|
|
|
assert.Nil(t, bc.AddBlock(b1))
|
|
block := bc.newBlock(tx2)
|
|
txSize := io.GetVarSize(tx2)
|
|
assert.Nil(t, bc.AddBlock(block))
|
|
|
|
// Test unpersisted and persisted access
|
|
for j := 0; j < 2; j++ {
|
|
tx, height, err := bc.GetTransaction(block.Transactions[0].Hash())
|
|
require.Nil(t, err)
|
|
assert.Equal(t, block.Index, height)
|
|
assert.Equal(t, txSize, tx.Size())
|
|
assert.Equal(t, block.Transactions[0], tx)
|
|
assert.Equal(t, 1, io.GetVarSize(tx.Attributes))
|
|
assert.Equal(t, 1, io.GetVarSize(tx.Scripts))
|
|
_, err = bc.persist(true)
|
|
assert.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
func TestGetClaimable(t *testing.T) {
|
|
bc := newTestChain(t)
|
|
|
|
_, err := bc.genBlocks(10)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("first generation period", func(t *testing.T) {
|
|
amount, err := bc.CalculateClaimable(neoOwner, 1)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, big.NewInt(5*native.GASFactor/10), amount)
|
|
})
|
|
}
|
|
|
|
func TestClose(t *testing.T) {
|
|
defer func() {
|
|
r := recover()
|
|
assert.NotNil(t, r)
|
|
}()
|
|
bc := initTestChain(t, nil, nil)
|
|
go bc.Run()
|
|
_, err := bc.genBlocks(10)
|
|
require.NoError(t, err)
|
|
bc.Close()
|
|
// It's a hack, but we use internal knowledge of MemoryStore
|
|
// implementation which makes it completely unusable (up to panicing)
|
|
// after Close().
|
|
_ = bc.dao.Store.Put([]byte{0}, []byte{1})
|
|
|
|
// This should never be executed.
|
|
assert.Nil(t, t)
|
|
}
|
|
|
|
func TestSubscriptions(t *testing.T) {
|
|
// We use buffering here as a substitute for reader goroutines, events
|
|
// get queued up and we read them one by one here.
|
|
const chBufSize = 16
|
|
blockCh := make(chan *block.Block, chBufSize)
|
|
txCh := make(chan *transaction.Transaction, chBufSize)
|
|
notificationCh := make(chan *subscriptions.NotificationEvent, chBufSize)
|
|
executionCh := make(chan *state.AppExecResult, chBufSize)
|
|
|
|
bc := newTestChain(t)
|
|
bc.SubscribeForBlocks(blockCh)
|
|
bc.SubscribeForTransactions(txCh)
|
|
bc.SubscribeForNotifications(notificationCh)
|
|
bc.SubscribeForExecutions(executionCh)
|
|
|
|
assert.Empty(t, notificationCh)
|
|
assert.Empty(t, executionCh)
|
|
assert.Empty(t, blockCh)
|
|
assert.Empty(t, txCh)
|
|
|
|
blocks, err := bc.genBlocks(1)
|
|
require.NoError(t, err)
|
|
require.Eventually(t, func() bool { return len(blockCh) != 0 }, time.Second, 10*time.Millisecond)
|
|
assert.Len(t, notificationCh, 1) // validator bounty
|
|
assert.Len(t, executionCh, 2)
|
|
assert.Empty(t, txCh)
|
|
|
|
b := <-blockCh
|
|
assert.Equal(t, blocks[0], b)
|
|
assert.Empty(t, blockCh)
|
|
|
|
aer := <-executionCh
|
|
assert.Equal(t, b.Hash(), aer.Container)
|
|
aer = <-executionCh
|
|
assert.Equal(t, b.Hash(), aer.Container)
|
|
|
|
notif := <-notificationCh
|
|
require.Equal(t, bc.UtilityTokenHash(), notif.ScriptHash)
|
|
|
|
script := io.NewBufBinWriter()
|
|
emit.Bytes(script.BinWriter, []byte("yay!"))
|
|
emit.Syscall(script.BinWriter, interopnames.SystemRuntimeNotify)
|
|
require.NoError(t, script.Err)
|
|
txGood1 := transaction.New(script.Bytes(), 0)
|
|
txGood1.Signers = []transaction.Signer{{Account: neoOwner}}
|
|
txGood1.Nonce = 1
|
|
txGood1.ValidUntilBlock = 1024
|
|
require.NoError(t, testchain.SignTx(bc, txGood1))
|
|
|
|
// Reset() reuses the script buffer and we need to keep scripts.
|
|
script = io.NewBufBinWriter()
|
|
emit.Bytes(script.BinWriter, []byte("nay!"))
|
|
emit.Syscall(script.BinWriter, interopnames.SystemRuntimeNotify)
|
|
emit.Opcodes(script.BinWriter, opcode.THROW)
|
|
require.NoError(t, script.Err)
|
|
txBad := transaction.New(script.Bytes(), 0)
|
|
txBad.Signers = []transaction.Signer{{Account: neoOwner}}
|
|
txBad.Nonce = 2
|
|
txBad.ValidUntilBlock = 1024
|
|
require.NoError(t, testchain.SignTx(bc, txBad))
|
|
|
|
script = io.NewBufBinWriter()
|
|
emit.Bytes(script.BinWriter, []byte("yay! yay! yay!"))
|
|
emit.Syscall(script.BinWriter, interopnames.SystemRuntimeNotify)
|
|
require.NoError(t, script.Err)
|
|
txGood2 := transaction.New(script.Bytes(), 0)
|
|
txGood2.Signers = []transaction.Signer{{Account: neoOwner}}
|
|
txGood2.Nonce = 3
|
|
txGood2.ValidUntilBlock = 1024
|
|
require.NoError(t, testchain.SignTx(bc, txGood2))
|
|
|
|
invBlock := newBlock(bc.config, bc.BlockHeight()+1, bc.CurrentHeaderHash(), txGood1, txBad, txGood2)
|
|
require.NoError(t, bc.AddBlock(invBlock))
|
|
|
|
require.Eventually(t, func() bool {
|
|
return len(blockCh) != 0 && len(txCh) != 0 &&
|
|
len(notificationCh) != 0 && len(executionCh) != 0
|
|
}, time.Second, 10*time.Millisecond)
|
|
|
|
b = <-blockCh
|
|
require.Equal(t, invBlock, b)
|
|
assert.Empty(t, blockCh)
|
|
|
|
exec := <-executionCh
|
|
require.Equal(t, b.Hash(), exec.Container)
|
|
require.Equal(t, exec.VMState, vm.HaltState)
|
|
|
|
// 3 burn events for every tx and 1 mint for primary node
|
|
require.True(t, len(notificationCh) >= 4)
|
|
for i := 0; i < 4; i++ {
|
|
notif := <-notificationCh
|
|
require.Equal(t, bc.contracts.GAS.Hash, notif.ScriptHash)
|
|
}
|
|
|
|
// Follow in-block transaction order.
|
|
for _, txExpected := range invBlock.Transactions {
|
|
tx := <-txCh
|
|
require.Equal(t, txExpected, tx)
|
|
exec := <-executionCh
|
|
require.Equal(t, tx.Hash(), exec.Container)
|
|
if exec.VMState == vm.HaltState {
|
|
notif := <-notificationCh
|
|
require.Equal(t, hash.Hash160(tx.Script), notif.ScriptHash)
|
|
}
|
|
}
|
|
assert.Empty(t, txCh)
|
|
assert.Len(t, notificationCh, 1)
|
|
assert.Len(t, executionCh, 1)
|
|
|
|
notif = <-notificationCh
|
|
require.Equal(t, bc.UtilityTokenHash(), notif.ScriptHash)
|
|
|
|
exec = <-executionCh
|
|
require.Equal(t, b.Hash(), exec.Container)
|
|
require.Equal(t, exec.VMState, vm.HaltState)
|
|
|
|
bc.UnsubscribeFromBlocks(blockCh)
|
|
bc.UnsubscribeFromTransactions(txCh)
|
|
bc.UnsubscribeFromNotifications(notificationCh)
|
|
bc.UnsubscribeFromExecutions(executionCh)
|
|
|
|
// Ensure that new blocks are processed correctly after unsubscription.
|
|
_, err = bc.genBlocks(2 * chBufSize)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testDumpAndRestore(t *testing.T, dumpF, restoreF func(c *config.Config)) {
|
|
if restoreF == nil {
|
|
restoreF = dumpF
|
|
}
|
|
|
|
bc := newTestChainWithCustomCfg(t, dumpF)
|
|
|
|
initBasicChain(t, bc)
|
|
require.True(t, bc.BlockHeight() > 5) // ensure that test is valid
|
|
|
|
w := io.NewBufBinWriter()
|
|
require.NoError(t, chaindump.Dump(bc, w.BinWriter, 0, bc.BlockHeight()+1))
|
|
require.NoError(t, w.Err)
|
|
|
|
buf := w.Bytes()
|
|
t.Run("invalid start", func(t *testing.T) {
|
|
bc2 := newTestChainWithCustomCfg(t, restoreF)
|
|
|
|
r := io.NewBinReaderFromBuf(buf)
|
|
require.Error(t, chaindump.Restore(bc2, r, 2, 1, nil))
|
|
})
|
|
t.Run("good", func(t *testing.T) {
|
|
bc2 := newTestChainWithCustomCfg(t, restoreF)
|
|
|
|
r := io.NewBinReaderFromBuf(buf)
|
|
require.NoError(t, chaindump.Restore(bc2, r, 0, 2, nil))
|
|
require.Equal(t, uint32(1), bc2.BlockHeight())
|
|
|
|
r = io.NewBinReaderFromBuf(buf) // new reader because start is relative to dump
|
|
require.NoError(t, chaindump.Restore(bc2, r, 2, 1, nil))
|
|
t.Run("check handler", func(t *testing.T) {
|
|
lastIndex := uint32(0)
|
|
errStopped := errors.New("stopped")
|
|
f := func(b *block.Block) error {
|
|
lastIndex = b.Index
|
|
if b.Index >= bc.BlockHeight()-1 {
|
|
return errStopped
|
|
}
|
|
return nil
|
|
}
|
|
require.NoError(t, chaindump.Restore(bc2, r, 0, 1, f))
|
|
require.Equal(t, bc2.BlockHeight(), lastIndex)
|
|
|
|
r = io.NewBinReaderFromBuf(buf)
|
|
err := chaindump.Restore(bc2, r, 4, bc.BlockHeight()-bc2.BlockHeight(), f)
|
|
require.True(t, errors.Is(err, errStopped))
|
|
require.Equal(t, bc.BlockHeight()-1, lastIndex)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestDumpAndRestore(t *testing.T) {
|
|
t.Run("no state root", func(t *testing.T) {
|
|
testDumpAndRestore(t, func(c *config.Config) {
|
|
c.ProtocolConfiguration.StateRootInHeader = false
|
|
}, nil)
|
|
})
|
|
t.Run("with state root", func(t *testing.T) {
|
|
testDumpAndRestore(t, func(c *config.Config) {
|
|
c.ProtocolConfiguration.StateRootInHeader = false
|
|
}, nil)
|
|
})
|
|
t.Run("remove untraceable", func(t *testing.T) {
|
|
// Dump can only be created if all blocks and transactions are present.
|
|
testDumpAndRestore(t, nil, func(c *config.Config) {
|
|
c.ProtocolConfiguration.MaxTraceableBlocks = 2
|
|
c.ProtocolConfiguration.RemoveUntraceableBlocks = true
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestRemoveUntraceable(t *testing.T) {
|
|
check := func(t *testing.T, bc *Blockchain, tHash, bHash util.Uint256, errorExpected bool) {
|
|
_, _, err := bc.GetTransaction(tHash)
|
|
if errorExpected {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
_, err = bc.GetAppExecResults(tHash, trigger.Application)
|
|
if errorExpected {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
_, err = bc.GetBlock(bHash)
|
|
if errorExpected {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
_, err = bc.GetHeader(bHash)
|
|
require.NoError(t, err)
|
|
}
|
|
t.Run("P2PStateExchangeExtensions off", func(t *testing.T) {
|
|
bc := newTestChainWithCustomCfg(t, func(c *config.Config) {
|
|
c.ProtocolConfiguration.MaxTraceableBlocks = 2
|
|
c.ProtocolConfiguration.RemoveUntraceableBlocks = true
|
|
})
|
|
|
|
tx1, err := testchain.NewTransferFromOwner(bc, bc.contracts.NEO.Hash, util.Uint160{}, 1, 0, bc.BlockHeight()+1)
|
|
require.NoError(t, err)
|
|
b1 := bc.newBlock(tx1)
|
|
require.NoError(t, bc.AddBlock(b1))
|
|
tx1Height := bc.BlockHeight()
|
|
|
|
tx2, err := testchain.NewTransferFromOwner(bc, bc.contracts.NEO.Hash, util.Uint160{}, 1, 0, bc.BlockHeight()+1)
|
|
require.NoError(t, err)
|
|
require.NoError(t, bc.AddBlock(bc.newBlock(tx2)))
|
|
|
|
_, h1, err := bc.GetTransaction(tx1.Hash())
|
|
require.NoError(t, err)
|
|
require.Equal(t, tx1Height, h1)
|
|
|
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
|
|
|
check(t, bc, tx1.Hash(), b1.Hash(), true)
|
|
})
|
|
t.Run("P2PStateExchangeExtensions on", func(t *testing.T) {
|
|
bc := newTestChainWithCustomCfg(t, func(c *config.Config) {
|
|
c.ProtocolConfiguration.MaxTraceableBlocks = 2
|
|
c.ProtocolConfiguration.RemoveUntraceableBlocks = true
|
|
c.ProtocolConfiguration.P2PStateExchangeExtensions = true
|
|
c.ProtocolConfiguration.StateSyncInterval = 2
|
|
c.ProtocolConfiguration.StateRootInHeader = true
|
|
})
|
|
|
|
tx1, err := testchain.NewTransferFromOwner(bc, bc.contracts.NEO.Hash, util.Uint160{}, 1, 0, bc.BlockHeight()+1)
|
|
require.NoError(t, err)
|
|
b1 := bc.newBlock(tx1)
|
|
require.NoError(t, bc.AddBlock(b1))
|
|
tx1Height := bc.BlockHeight()
|
|
|
|
tx2, err := testchain.NewTransferFromOwner(bc, bc.contracts.NEO.Hash, util.Uint160{}, 1, 0, bc.BlockHeight()+1)
|
|
require.NoError(t, err)
|
|
b2 := bc.newBlock(tx2)
|
|
require.NoError(t, bc.AddBlock(b2))
|
|
tx2Height := bc.BlockHeight()
|
|
|
|
_, h1, err := bc.GetTransaction(tx1.Hash())
|
|
require.NoError(t, err)
|
|
require.Equal(t, tx1Height, h1)
|
|
|
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
|
|
|
check(t, bc, tx1.Hash(), b1.Hash(), false)
|
|
check(t, bc, tx2.Hash(), b2.Hash(), false)
|
|
|
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
|
|
|
check(t, bc, tx1.Hash(), b1.Hash(), true)
|
|
check(t, bc, tx2.Hash(), b2.Hash(), false)
|
|
_, h2, err := bc.GetTransaction(tx2.Hash())
|
|
require.NoError(t, err)
|
|
require.Equal(t, tx2Height, h2)
|
|
})
|
|
}
|
|
|
|
func TestInvalidNotification(t *testing.T) {
|
|
bc := newTestChain(t)
|
|
|
|
cs, _ := getTestContractState(bc)
|
|
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs))
|
|
|
|
aer, err := invokeContractMethod(bc, 1_00000000, cs.Hash, "invalidStack")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, len(aer.Stack))
|
|
require.Nil(t, aer.Stack[0])
|
|
require.Equal(t, stackitem.InteropT, aer.Stack[1].Type())
|
|
}
|
|
|
|
// Test that deletion of non-existent doesn't result in error in tx or block addition.
|
|
func TestMPTDeleteNoKey(t *testing.T) {
|
|
bc := newTestChain(t)
|
|
|
|
cs, _ := getTestContractState(bc)
|
|
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs))
|
|
aer, err := invokeContractMethod(bc, 1_00000000, cs.Hash, "delValue", "non-existent-key")
|
|
require.NoError(t, err)
|
|
require.Equal(t, vm.HaltState, aer.VMState)
|
|
}
|
|
|
|
// Test that UpdateHistory is added to ProtocolConfiguration for all native contracts
|
|
// for all default configurations. If UpdateHistory is not added to config, then
|
|
// native contract is disabled. It's easy to forget about config while adding new
|
|
// native contract.
|
|
func TestConfigNativeUpdateHistory(t *testing.T) {
|
|
var prefixPath = filepath.Join("..", "..", "config")
|
|
check := func(t *testing.T, cfgFileSuffix interface{}) {
|
|
cfgPath := filepath.Join(prefixPath, fmt.Sprintf("protocol.%s.yml", cfgFileSuffix))
|
|
cfg, err := config.LoadFile(cfgPath)
|
|
require.NoError(t, err, fmt.Errorf("failed to load %s", cfgPath))
|
|
natives := native.NewContracts(cfg.ProtocolConfiguration)
|
|
assert.Equal(t, len(natives.Contracts),
|
|
len(cfg.ProtocolConfiguration.NativeUpdateHistories),
|
|
fmt.Errorf("protocol configuration file %s: extra or missing NativeUpdateHistory in NativeActivations section", cfgPath))
|
|
for _, c := range natives.Contracts {
|
|
assert.NotNil(t, cfg.ProtocolConfiguration.NativeUpdateHistories[c.Metadata().Name],
|
|
fmt.Errorf("protocol configuration file %s: configuration for %s native contract is missing in NativeActivations section; "+
|
|
"edit the test if the contract should be disabled", cfgPath, c.Metadata().Name))
|
|
}
|
|
}
|
|
testCases := []interface{}{
|
|
netmode.MainNet,
|
|
netmode.PrivNet,
|
|
netmode.TestNet,
|
|
netmode.UnitTestNet,
|
|
"privnet.docker.one",
|
|
"privnet.docker.two",
|
|
"privnet.docker.three",
|
|
"privnet.docker.four",
|
|
"privnet.docker.single",
|
|
"unit_testnet.single",
|
|
}
|
|
for _, tc := range testCases {
|
|
check(t, tc)
|
|
}
|
|
}
|
|
|
|
func TestBlockchain_InitWithIncompleteStateJump(t *testing.T) {
|
|
var (
|
|
stateSyncInterval = 4
|
|
maxTraceable uint32 = 6
|
|
)
|
|
spountCfg := func(c *config.Config) {
|
|
c.ProtocolConfiguration.RemoveUntraceableBlocks = true
|
|
c.ProtocolConfiguration.StateRootInHeader = true
|
|
c.ProtocolConfiguration.P2PStateExchangeExtensions = true
|
|
c.ProtocolConfiguration.StateSyncInterval = stateSyncInterval
|
|
c.ProtocolConfiguration.MaxTraceableBlocks = maxTraceable
|
|
}
|
|
bcSpout := newTestChainWithCustomCfg(t, spountCfg)
|
|
initBasicChain(t, bcSpout)
|
|
|
|
// reach next to the latest state sync point and pretend that we've just restored
|
|
stateSyncPoint := (int(bcSpout.BlockHeight())/stateSyncInterval + 1) * stateSyncInterval
|
|
for i := bcSpout.BlockHeight() + 1; i <= uint32(stateSyncPoint); i++ {
|
|
require.NoError(t, bcSpout.AddBlock(bcSpout.newBlock()))
|
|
}
|
|
require.Equal(t, uint32(stateSyncPoint), bcSpout.BlockHeight())
|
|
b := bcSpout.newBlock()
|
|
require.NoError(t, bcSpout.AddHeaders(&b.Header))
|
|
|
|
// put storage items with STTemp prefix
|
|
batch := bcSpout.dao.Store.Batch()
|
|
tempPrefix := storage.STTempStorage
|
|
if bcSpout.dao.Version.StoragePrefix == tempPrefix {
|
|
tempPrefix = storage.STStorage
|
|
}
|
|
bcSpout.dao.Store.Seek(bcSpout.dao.Version.StoragePrefix.Bytes(), func(k, v []byte) {
|
|
key := slice.Copy(k)
|
|
key[0] = byte(tempPrefix)
|
|
value := slice.Copy(v)
|
|
batch.Put(key, value)
|
|
})
|
|
require.NoError(t, bcSpout.dao.Store.PutBatch(batch))
|
|
|
|
checkNewBlockchainErr := func(t *testing.T, cfg func(c *config.Config), store storage.Store, shouldFail bool) {
|
|
unitTestNetCfg, err := config.Load("../../config", testchain.Network())
|
|
require.NoError(t, err)
|
|
cfg(&unitTestNetCfg)
|
|
log := zaptest.NewLogger(t)
|
|
_, err = NewBlockchain(store, unitTestNetCfg.ProtocolConfiguration, log)
|
|
if shouldFail {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
boltCfg := func(c *config.Config) {
|
|
spountCfg(c)
|
|
c.ProtocolConfiguration.KeepOnlyLatestState = true
|
|
}
|
|
// manually store statejump stage to check statejump recover process
|
|
t.Run("invalid RemoveUntraceableBlocks setting", func(t *testing.T) {
|
|
require.NoError(t, bcSpout.dao.Store.Put(storage.SYSStateJumpStage.Bytes(), []byte{byte(stateJumpStarted)}))
|
|
checkNewBlockchainErr(t, func(c *config.Config) {
|
|
boltCfg(c)
|
|
c.ProtocolConfiguration.RemoveUntraceableBlocks = false
|
|
}, bcSpout.dao.Store, true)
|
|
})
|
|
t.Run("invalid state jump stage format", func(t *testing.T) {
|
|
require.NoError(t, bcSpout.dao.Store.Put(storage.SYSStateJumpStage.Bytes(), []byte{0x01, 0x02}))
|
|
checkNewBlockchainErr(t, boltCfg, bcSpout.dao.Store, true)
|
|
})
|
|
t.Run("missing state sync point", func(t *testing.T) {
|
|
require.NoError(t, bcSpout.dao.Store.Put(storage.SYSStateJumpStage.Bytes(), []byte{byte(stateJumpStarted)}))
|
|
checkNewBlockchainErr(t, boltCfg, bcSpout.dao.Store, true)
|
|
})
|
|
t.Run("invalid state sync point", func(t *testing.T) {
|
|
require.NoError(t, bcSpout.dao.Store.Put(storage.SYSStateJumpStage.Bytes(), []byte{byte(stateJumpStarted)}))
|
|
point := make([]byte, 4)
|
|
binary.LittleEndian.PutUint32(point, uint32(len(bcSpout.headerHashes)))
|
|
require.NoError(t, bcSpout.dao.Store.Put(storage.SYSStateSyncPoint.Bytes(), point))
|
|
checkNewBlockchainErr(t, boltCfg, bcSpout.dao.Store, true)
|
|
})
|
|
for _, stage := range []stateJumpStage{stateJumpStarted, newStorageItemsAdded, genesisStateRemoved, 0x03} {
|
|
t.Run(fmt.Sprintf("state jump stage %d", stage), func(t *testing.T) {
|
|
require.NoError(t, bcSpout.dao.Store.Put(storage.SYSStateJumpStage.Bytes(), []byte{byte(stage)}))
|
|
point := make([]byte, 4)
|
|
binary.LittleEndian.PutUint32(point, uint32(stateSyncPoint))
|
|
require.NoError(t, bcSpout.dao.Store.Put(storage.SYSStateSyncPoint.Bytes(), point))
|
|
shouldFail := stage == 0x03 // unknown stage
|
|
checkNewBlockchainErr(t, spountCfg, bcSpout.dao.Store, shouldFail)
|
|
})
|
|
}
|
|
}
|