mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-23 03:38:35 +00:00
339 lines
12 KiB
Go
339 lines
12 KiB
Go
|
package core_test
|
||
|
|
||
|
import (
|
||
|
"encoding/binary"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"strings"
|
||
|
"testing"
|
||
|
|
||
|
"github.com/nspcc-dev/neo-go/pkg/config"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/core/chaindump"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/core/dao"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/neotest"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/neotest/chain"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||
|
"github.com/stretchr/testify/require"
|
||
|
)
|
||
|
|
||
|
func TestBlockchain_DumpAndRestore(t *testing.T) {
|
||
|
t.Run("no state root", func(t *testing.T) {
|
||
|
testDumpAndRestore(t, func(c *config.ProtocolConfiguration) {
|
||
|
c.StateRootInHeader = false
|
||
|
c.P2PSigExtensions = true
|
||
|
}, nil)
|
||
|
})
|
||
|
t.Run("with state root", func(t *testing.T) {
|
||
|
testDumpAndRestore(t, func(c *config.ProtocolConfiguration) {
|
||
|
c.StateRootInHeader = true
|
||
|
c.P2PSigExtensions = true
|
||
|
}, nil)
|
||
|
})
|
||
|
t.Run("remove untraceable", func(t *testing.T) {
|
||
|
// Dump can only be created if all blocks and transactions are present.
|
||
|
testDumpAndRestore(t, func(c *config.ProtocolConfiguration) {
|
||
|
c.P2PSigExtensions = true
|
||
|
}, func(c *config.ProtocolConfiguration) {
|
||
|
c.MaxTraceableBlocks = 2
|
||
|
c.RemoveUntraceableBlocks = true
|
||
|
c.P2PSigExtensions = true
|
||
|
})
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func testDumpAndRestore(t *testing.T, dumpF, restoreF func(c *config.ProtocolConfiguration)) {
|
||
|
if restoreF == nil {
|
||
|
restoreF = dumpF
|
||
|
}
|
||
|
|
||
|
bc, validators, committee := chain.NewMultiWithCustomConfig(t, dumpF)
|
||
|
e := neotest.NewExecutor(t, bc, validators, committee)
|
||
|
|
||
|
initBasicChain(t, e)
|
||
|
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, _, _ := chain.NewMultiWithCustomConfig(t, restoreF)
|
||
|
|
||
|
r := io.NewBinReaderFromBuf(buf)
|
||
|
require.Error(t, chaindump.Restore(bc2, r, 2, 1, nil))
|
||
|
})
|
||
|
t.Run("good", func(t *testing.T) {
|
||
|
bc2, _, _ := chain.NewMultiWithCustomConfig(t, dumpF)
|
||
|
|
||
|
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 newLevelDBForTestingWithPath(t testing.TB, dbPath string) (storage.Store, string) {
|
||
|
if dbPath == "" {
|
||
|
dbPath = t.TempDir()
|
||
|
}
|
||
|
dbOptions := storage.LevelDBOptions{
|
||
|
DataDirectoryPath: dbPath,
|
||
|
}
|
||
|
newLevelStore, err := storage.NewLevelDBStore(dbOptions)
|
||
|
require.Nil(t, err, "NewLevelDBStore error")
|
||
|
return newLevelStore, dbPath
|
||
|
}
|
||
|
|
||
|
func TestBlockchain_StartFromExistingDB(t *testing.T) {
|
||
|
ps, path := newLevelDBForTestingWithPath(t, "")
|
||
|
customConfig := func(c *config.ProtocolConfiguration) {
|
||
|
c.StateRootInHeader = true // Need for P2PStateExchangeExtensions check.
|
||
|
c.P2PSigExtensions = true // Need for basic chain initializer.
|
||
|
}
|
||
|
bc, validators, committee, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, ps)
|
||
|
require.NoError(t, err)
|
||
|
go bc.Run()
|
||
|
e := neotest.NewExecutor(t, bc, validators, committee)
|
||
|
initBasicChain(t, e)
|
||
|
require.True(t, bc.BlockHeight() > 5, "ensure that basic chain is correctly initialised")
|
||
|
|
||
|
// Information for further tests.
|
||
|
h := bc.BlockHeight()
|
||
|
cryptoLibHash, err := bc.GetNativeContractScriptHash(nativenames.CryptoLib)
|
||
|
require.NoError(t, err)
|
||
|
cryptoLibState := bc.GetContractState(cryptoLibHash)
|
||
|
require.NotNil(t, cryptoLibState)
|
||
|
var (
|
||
|
managementID = -1
|
||
|
managementContractPrefix = 8
|
||
|
)
|
||
|
|
||
|
bc.Close() // Ensure persist is done and persistent store is properly closed.
|
||
|
|
||
|
newPS := func(t *testing.T) storage.Store {
|
||
|
ps, _ = newLevelDBForTestingWithPath(t, path)
|
||
|
t.Cleanup(func() { require.NoError(t, ps.Close()) })
|
||
|
return ps
|
||
|
}
|
||
|
t.Run("mismatch storage version", func(t *testing.T) {
|
||
|
ps = newPS(t)
|
||
|
cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption.
|
||
|
d := dao.NewSimple(cache, bc.GetConfig().StateRootInHeader, bc.GetConfig().P2PStateExchangeExtensions)
|
||
|
d.PutVersion(dao.Version{
|
||
|
Value: "0.0.0",
|
||
|
})
|
||
|
_, err := d.Persist() // Persist to `cache` wrapper.
|
||
|
require.NoError(t, err)
|
||
|
_, _, _, err = chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache)
|
||
|
require.Error(t, err)
|
||
|
require.True(t, strings.Contains(err.Error(), "storage version mismatch"), err)
|
||
|
})
|
||
|
t.Run("mismatch StateRootInHeader", func(t *testing.T) {
|
||
|
ps = newPS(t)
|
||
|
_, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, func(c *config.ProtocolConfiguration) {
|
||
|
customConfig(c)
|
||
|
c.StateRootInHeader = false
|
||
|
}, ps)
|
||
|
require.Error(t, err)
|
||
|
require.True(t, strings.Contains(err.Error(), "StateRootInHeader setting mismatch"), err)
|
||
|
})
|
||
|
t.Run("mismatch P2PSigExtensions", func(t *testing.T) {
|
||
|
ps = newPS(t)
|
||
|
_, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, func(c *config.ProtocolConfiguration) {
|
||
|
customConfig(c)
|
||
|
c.P2PSigExtensions = false
|
||
|
}, ps)
|
||
|
require.Error(t, err)
|
||
|
require.True(t, strings.Contains(err.Error(), "P2PSigExtensions setting mismatch"), err)
|
||
|
})
|
||
|
t.Run("mismatch P2PStateExchangeExtensions", func(t *testing.T) {
|
||
|
ps = newPS(t)
|
||
|
_, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, func(c *config.ProtocolConfiguration) {
|
||
|
customConfig(c)
|
||
|
c.StateRootInHeader = true
|
||
|
c.P2PStateExchangeExtensions = true
|
||
|
}, ps)
|
||
|
require.Error(t, err)
|
||
|
require.True(t, strings.Contains(err.Error(), "P2PStateExchangeExtensions setting mismatch"), err)
|
||
|
})
|
||
|
t.Run("mismatch KeepOnlyLatestState", func(t *testing.T) {
|
||
|
ps = newPS(t)
|
||
|
_, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, func(c *config.ProtocolConfiguration) {
|
||
|
customConfig(c)
|
||
|
c.KeepOnlyLatestState = true
|
||
|
}, ps)
|
||
|
require.Error(t, err)
|
||
|
require.True(t, strings.Contains(err.Error(), "KeepOnlyLatestState setting mismatch"), err)
|
||
|
})
|
||
|
t.Run("corrupted headers", func(t *testing.T) {
|
||
|
ps = newPS(t)
|
||
|
|
||
|
// Corrupt headers hashes batch.
|
||
|
cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption.
|
||
|
key := make([]byte, 5)
|
||
|
key[0] = byte(storage.IXHeaderHashList)
|
||
|
binary.BigEndian.PutUint32(key[1:], 1)
|
||
|
cache.Put(key, []byte{1, 2, 3})
|
||
|
|
||
|
_, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache)
|
||
|
require.Error(t, err)
|
||
|
require.True(t, strings.Contains(err.Error(), "failed to read batch of 2000"), err)
|
||
|
})
|
||
|
t.Run("corrupted current header height", func(t *testing.T) {
|
||
|
ps = newPS(t)
|
||
|
|
||
|
// Remove current header.
|
||
|
cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption.
|
||
|
cache.Delete([]byte{byte(storage.SYSCurrentHeader)})
|
||
|
|
||
|
_, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache)
|
||
|
require.Error(t, err)
|
||
|
require.True(t, strings.Contains(err.Error(), "failed to retrieve current header"), err)
|
||
|
})
|
||
|
t.Run("missing last batch of 2000 headers and missing last header", func(t *testing.T) {
|
||
|
ps = newPS(t)
|
||
|
|
||
|
// Remove latest headers hashes batch and current header.
|
||
|
cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption.
|
||
|
cache.Delete([]byte{byte(storage.IXHeaderHashList)})
|
||
|
currHeaderInfo, err := cache.Get([]byte{byte(storage.SYSCurrentHeader)})
|
||
|
require.NoError(t, err)
|
||
|
currHeaderHash, err := util.Uint256DecodeBytesLE(currHeaderInfo[:32])
|
||
|
require.NoError(t, err)
|
||
|
cache.Delete(append([]byte{byte(storage.DataExecutable)}, currHeaderHash.BytesBE()...))
|
||
|
|
||
|
_, _, _, err = chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache)
|
||
|
require.Error(t, err)
|
||
|
require.True(t, strings.Contains(err.Error(), "could not get header"), err)
|
||
|
})
|
||
|
t.Run("missing last block", func(t *testing.T) {
|
||
|
ps = newPS(t)
|
||
|
|
||
|
// Remove current block from storage.
|
||
|
cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption.
|
||
|
cache.Delete([]byte{byte(storage.SYSCurrentBlock)})
|
||
|
|
||
|
_, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache)
|
||
|
require.Error(t, err)
|
||
|
require.True(t, strings.Contains(err.Error(), "failed to retrieve current block height"), err)
|
||
|
})
|
||
|
t.Run("missing last stateroot", func(t *testing.T) {
|
||
|
ps = newPS(t)
|
||
|
|
||
|
// Remove latest stateroot from storage.
|
||
|
cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption.
|
||
|
key := make([]byte, 5)
|
||
|
key[0] = byte(storage.DataMPTAux)
|
||
|
binary.BigEndian.PutUint32(key, h)
|
||
|
cache.Delete(key)
|
||
|
|
||
|
_, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache)
|
||
|
require.Error(t, err)
|
||
|
require.True(t, strings.Contains(err.Error(), "can't init MPT at height"), err)
|
||
|
})
|
||
|
t.Run("failed native Management initialisation", func(t *testing.T) {
|
||
|
ps = newPS(t)
|
||
|
|
||
|
// Corrupt serialised CryptoLib state.
|
||
|
cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption.
|
||
|
key := make([]byte, 1+4+1+20)
|
||
|
key[0] = byte(storage.STStorage)
|
||
|
binary.LittleEndian.PutUint32(key[1:], uint32(managementID))
|
||
|
key[5] = byte(managementContractPrefix)
|
||
|
copy(key[6:], cryptoLibHash.BytesBE())
|
||
|
cache.Put(key, []byte{1, 2, 3})
|
||
|
|
||
|
_, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache)
|
||
|
require.Error(t, err)
|
||
|
require.True(t, strings.Contains(err.Error(), "can't init cache for Management native contract"), err)
|
||
|
})
|
||
|
t.Run("invalid native contract deactivation", func(t *testing.T) {
|
||
|
ps = newPS(t)
|
||
|
_, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, func(c *config.ProtocolConfiguration) {
|
||
|
customConfig(c)
|
||
|
c.NativeUpdateHistories = map[string][]uint32{
|
||
|
nativenames.Policy: {0},
|
||
|
nativenames.Neo: {0},
|
||
|
nativenames.Gas: {0},
|
||
|
nativenames.Designation: {0},
|
||
|
nativenames.StdLib: {0},
|
||
|
nativenames.Management: {0},
|
||
|
nativenames.Oracle: {0},
|
||
|
nativenames.Ledger: {0},
|
||
|
nativenames.Notary: {0},
|
||
|
nativenames.CryptoLib: {h + 10},
|
||
|
}
|
||
|
}, ps)
|
||
|
require.Error(t, err)
|
||
|
require.True(t, strings.Contains(err.Error(), fmt.Sprintf("native contract %s is already stored, but marked as inactive for height %d in config", nativenames.CryptoLib, h)), err)
|
||
|
})
|
||
|
t.Run("invalid native contract activation", func(t *testing.T) {
|
||
|
ps = newPS(t)
|
||
|
|
||
|
// Remove CryptoLib from storage.
|
||
|
cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption.
|
||
|
key := make([]byte, 1+4+1+20)
|
||
|
key[0] = byte(storage.STStorage)
|
||
|
binary.LittleEndian.PutUint32(key[1:], uint32(managementID))
|
||
|
key[5] = byte(managementContractPrefix)
|
||
|
copy(key[6:], cryptoLibHash.BytesBE())
|
||
|
cache.Delete(key)
|
||
|
|
||
|
_, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache)
|
||
|
require.Error(t, err)
|
||
|
require.True(t, strings.Contains(err.Error(), fmt.Sprintf("native contract %s is not stored, but should be active at height %d according to config", nativenames.CryptoLib, h)), err)
|
||
|
})
|
||
|
t.Run("stored and autogenerated native contract's states mismatch", func(t *testing.T) {
|
||
|
ps = newPS(t)
|
||
|
|
||
|
// Change stored CryptoLib state.
|
||
|
cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption.
|
||
|
key := make([]byte, 1+4+1+20)
|
||
|
key[0] = byte(storage.STStorage)
|
||
|
binary.LittleEndian.PutUint32(key[1:], uint32(managementID))
|
||
|
key[5] = byte(managementContractPrefix)
|
||
|
copy(key[6:], cryptoLibHash.BytesBE())
|
||
|
cs := *cryptoLibState
|
||
|
cs.ID = -123
|
||
|
csBytes, err := stackitem.SerializeConvertible(&cs)
|
||
|
require.NoError(t, err)
|
||
|
cache.Put(key, csBytes)
|
||
|
|
||
|
_, _, _, err = chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache)
|
||
|
require.Error(t, err)
|
||
|
require.True(t, strings.Contains(err.Error(), fmt.Sprintf("native %s: version mismatch (stored contract state differs from autogenerated one)", nativenames.CryptoLib)), err)
|
||
|
})
|
||
|
|
||
|
t.Run("good", func(t *testing.T) {
|
||
|
ps = newPS(t)
|
||
|
_, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, ps)
|
||
|
require.NoError(t, err)
|
||
|
})
|
||
|
}
|