neo-go/pkg/consensus/consensus_test.go
Roman Khimov 8d33206bb8 *: don't get private key from account if just public one is needed
Add PublicKey() API to the Account and use it as appropriate, avoid creating
additional references to the private key.
2022-09-02 14:43:28 +03:00

564 lines
17 KiB
Go

package consensus
import (
"errors"
"testing"
"time"
"github.com/nspcc-dev/dbft/block"
"github.com/nspcc-dev/dbft/payload"
"github.com/nspcc-dev/dbft/timer"
"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"
"github.com/nspcc-dev/neo-go/pkg/core/fee"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"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/io"
npayload "github.com/nspcc-dev/neo-go/pkg/network/payload"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
"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/wallet"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
)
func TestNewService(t *testing.T) {
srv := newTestService(t)
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 100000)
tx.ValidUntilBlock = 1
addSender(t, tx)
signTx(t, srv.Chain, tx)
require.NoError(t, srv.Chain.PoolTx(tx))
var txx []block.Transaction
require.NotPanics(t, func() { txx = srv.getVerifiedTx() })
require.Len(t, txx, 1)
require.Equal(t, tx, txx[0])
}
func initServiceNextConsensus(t *testing.T, newAcc *wallet.Account, offset uint32) (*service, *wallet.Account) {
acc, err := wallet.NewAccountFromWIF(testchain.WIF(testchain.IDToOrder(0)))
require.NoError(t, err)
priv := acc.PrivateKey()
require.NoError(t, acc.ConvertMultisig(1, keys.PublicKeys{priv.PublicKey()}))
bc := newSingleTestChain(t)
newPriv := newAcc.PrivateKey()
// Transfer funds to new validator.
b := smartcontract.NewBuilder()
b.InvokeWithAssert(bc.GoverningTokenHash(), "transfer",
acc.Contract.ScriptHash().BytesBE(), newPriv.GetScriptHash().BytesBE(), int64(native.NEOTotalSupply), nil)
b.InvokeWithAssert(bc.UtilityTokenHash(), "transfer",
acc.Contract.ScriptHash().BytesBE(), newPriv.GetScriptHash().BytesBE(), int64(10000_000_000_000), nil)
script, err := b.Script()
require.NoError(t, err)
tx := transaction.New(script, 21_000_000)
tx.ValidUntilBlock = bc.BlockHeight() + 1
tx.NetworkFee = 10_000_000
tx.Signers = []transaction.Signer{{Scopes: transaction.Global, Account: acc.Contract.ScriptHash()}}
require.NoError(t, acc.SignTx(netmode.UnitTestNet, tx))
require.NoError(t, bc.PoolTx(tx))
srv := newTestServiceWithChain(t, bc)
srv.dbft.Start()
// Register new candidate.
b.Reset()
b.InvokeWithAssert(bc.GoverningTokenHash(), "registerCandidate", newPriv.PublicKey().Bytes())
script, err = b.Script()
require.NoError(t, err)
tx = transaction.New(script, 1001_00000000)
tx.ValidUntilBlock = bc.BlockHeight() + 1
tx.NetworkFee = 20_000_000
tx.Signers = []transaction.Signer{{Scopes: transaction.Global, Account: newPriv.GetScriptHash()}}
require.NoError(t, newAcc.SignTx(netmode.UnitTestNet, tx))
require.NoError(t, bc.PoolTx(tx))
srv.dbft.OnTimeout(timer.HV{Height: srv.dbft.Context.BlockIndex})
cfg := bc.GetConfig()
for i := srv.dbft.BlockIndex; !cfg.ShouldUpdateCommitteeAt(i + offset); i++ {
srv.dbft.OnTimeout(timer.HV{Height: srv.dbft.Context.BlockIndex})
}
// Vote for new candidate.
b.Reset()
b.InvokeWithAssert(bc.GoverningTokenHash(), "vote",
newPriv.GetScriptHash(), newPriv.PublicKey().Bytes())
script, err = b.Script()
require.NoError(t, err)
tx = transaction.New(script, 20_000_000)
tx.ValidUntilBlock = bc.BlockHeight() + 1
tx.NetworkFee = 20_000_000
tx.Signers = []transaction.Signer{{Scopes: transaction.Global, Account: newPriv.GetScriptHash()}}
require.NoError(t, newAcc.SignTx(netmode.UnitTestNet, tx))
require.NoError(t, bc.PoolTx(tx))
srv.dbft.OnTimeout(timer.HV{Height: srv.dbft.BlockIndex})
return srv, acc
}
func TestService_NextConsensus(t *testing.T) {
newAcc, err := wallet.NewAccount()
require.NoError(t, err)
script, err := smartcontract.CreateMajorityMultiSigRedeemScript(keys.PublicKeys{newAcc.PublicKey()})
require.NoError(t, err)
checkNextConsensus := func(t *testing.T, bc *core.Blockchain, height uint32, h util.Uint160) {
hdrHash := bc.GetHeaderHash(int(height))
hdr, err := bc.GetHeader(hdrHash)
require.NoError(t, err)
require.Equal(t, h, hdr.NextConsensus)
}
t.Run("vote 1 block before update", func(t *testing.T) { // voting occurs every block in SingleTestChain
srv, acc := initServiceNextConsensus(t, newAcc, 1)
bc := srv.Chain.(*core.Blockchain)
height := bc.BlockHeight()
checkNextConsensus(t, bc, height, acc.Contract.ScriptHash())
// Reset <- we are here, update NextConsensus
// OnPersist <- update committee
// Block <-
srv.dbft.OnTimeout(timer.HV{Height: srv.dbft.BlockIndex})
checkNextConsensus(t, bc, height+1, hash.Hash160(script))
})
/*
t.Run("vote 2 blocks before update", func(t *testing.T) {
srv, acc := initServiceNextConsensus(t, newAcc, 2)
bc := srv.Chain.(*core.Blockchain)
defer bc.Close()
height := bc.BlockHeight()
checkNextConsensus(t, bc, height, acc.Contract.ScriptHash())
// Reset <- we are here
// OnPersist <- nothing to do
// Block <-
//
// Reset <- update next consensus
// OnPersist <- update committee
// Block <-
srv.dbft.OnTimeout(timer.HV{Height: srv.dbft.BlockIndex})
checkNextConsensus(t, bc, height+1, acc.Contract.ScriptHash())
srv.dbft.OnTimeout(timer.HV{Height: srv.dbft.BlockIndex})
checkNextConsensus(t, bc, height+2, hash.Hash160(script))
})
*/
}
func TestService_GetVerified(t *testing.T) {
srv := newTestService(t)
srv.dbft.Start()
var txs []*transaction.Transaction
for i := 0; i < 4; i++ {
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 100000)
tx.Nonce = 123 + uint32(i)
tx.ValidUntilBlock = 1
txs = append(txs, tx)
}
addSender(t, txs...)
signTx(t, srv.Chain, txs...)
require.NoError(t, srv.Chain.PoolTx(txs[3]))
hashes := []util.Uint256{txs[0].Hash(), txs[1].Hash(), txs[2].Hash()}
// Everyone sends a message.
for i := 0; i < 4; i++ {
p := new(Payload)
// One PrepareRequest and three ChangeViews.
if i == 1 {
p.SetType(payload.PrepareRequestType)
p.SetPayload(&prepareRequest{prevHash: srv.Chain.CurrentBlockHash(), transactionHashes: hashes})
} else {
p.SetType(payload.ChangeViewType)
p.SetPayload(&changeView{newViewNumber: 1, timestamp: uint64(time.Now().UnixNano() / nsInMs)})
}
p.SetHeight(1)
p.SetValidatorIndex(uint16(i))
priv, _ := getTestValidator(i)
require.NoError(t, p.Sign(priv))
// Skip srv.OnPayload, because the service is not really started.
srv.dbft.OnReceive(p)
}
require.Equal(t, uint8(1), srv.dbft.ViewNumber)
require.Equal(t, hashes, srv.lastProposal)
t.Run("new transactions will be proposed in case of failure", func(t *testing.T) {
txx := srv.getVerifiedTx()
require.Equal(t, 1, len(txx), "there is only 1 tx in mempool")
require.Equal(t, txs[3], txx[0])
})
t.Run("more than half of the last proposal will be reused", func(t *testing.T) {
for _, tx := range txs[:2] {
require.NoError(t, srv.Chain.PoolTx(tx))
}
txx := srv.getVerifiedTx()
require.Contains(t, txx, txs[0])
require.Contains(t, txx, txs[1])
require.NotContains(t, txx, txs[2])
})
}
func TestService_ValidatePayload(t *testing.T) {
srv := newTestService(t)
priv, _ := getTestValidator(1)
p := new(Payload)
p.Sender = priv.GetScriptHash()
p.SetPayload(&prepareRequest{})
t.Run("invalid validator index", func(t *testing.T) {
p.SetValidatorIndex(11)
require.NoError(t, p.Sign(priv))
var ok bool
require.NotPanics(t, func() { ok = srv.validatePayload(p) })
require.False(t, ok)
})
t.Run("wrong validator index", func(t *testing.T) {
p.SetValidatorIndex(2)
require.NoError(t, p.Sign(priv))
require.False(t, srv.validatePayload(p))
})
t.Run("invalid sender", func(t *testing.T) {
p.SetValidatorIndex(1)
p.Sender = util.Uint160{}
require.NoError(t, p.Sign(priv))
require.False(t, srv.validatePayload(p))
})
t.Run("normal case", func(t *testing.T) {
p.SetValidatorIndex(1)
p.Sender = priv.GetScriptHash()
require.NoError(t, p.Sign(priv))
require.True(t, srv.validatePayload(p))
})
}
func TestService_getTx(t *testing.T) {
srv := newTestService(t)
t.Run("transaction in mempool", func(t *testing.T) {
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
tx.Nonce = 1234
tx.ValidUntilBlock = 1
addSender(t, tx)
signTx(t, srv.Chain, tx)
h := tx.Hash()
require.Equal(t, nil, srv.getTx(h))
require.NoError(t, srv.Chain.PoolTx(tx))
got := srv.getTx(h)
require.NotNil(t, got)
require.Equal(t, h, got.Hash())
})
t.Run("transaction in local cache", func(t *testing.T) {
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
tx.Nonce = 4321
tx.ValidUntilBlock = 1
h := tx.Hash()
require.Equal(t, nil, srv.getTx(h))
srv.txx.Add(tx)
got := srv.getTx(h)
require.NotNil(t, got)
require.Equal(t, h, got.Hash())
})
}
func TestService_PrepareRequest(t *testing.T) {
srv := newTestServiceWithState(t, true)
srv.dbft.Start()
t.Cleanup(srv.dbft.Timer.Stop)
priv, _ := getTestValidator(1)
p := new(Payload)
p.SetValidatorIndex(1)
prevHash := srv.Chain.CurrentBlockHash()
checkRequest := func(t *testing.T, expectedErr error, req *prepareRequest) {
p.SetPayload(req)
require.NoError(t, p.Sign(priv))
err := srv.verifyRequest(p)
if expectedErr == nil {
require.NoError(t, err)
return
}
require.True(t, errors.Is(err, expectedErr), "got: %v", err)
}
checkRequest(t, errInvalidVersion, &prepareRequest{version: 0xFF, prevHash: prevHash})
checkRequest(t, errInvalidPrevHash, &prepareRequest{prevHash: random.Uint256()})
checkRequest(t, errInvalidStateRoot, &prepareRequest{
stateRootEnabled: true,
prevHash: prevHash,
})
sr, err := srv.Chain.GetStateRoot(srv.dbft.BlockIndex - 1)
require.NoError(t, err)
checkRequest(t, errInvalidTransactionsCount, &prepareRequest{stateRootEnabled: true,
prevHash: prevHash,
stateRoot: sr.Root,
transactionHashes: make([]util.Uint256, srv.ProtocolConfiguration.MaxTransactionsPerBlock+1),
})
checkRequest(t, nil, &prepareRequest{
stateRootEnabled: true,
prevHash: prevHash,
stateRoot: sr.Root,
})
}
func TestService_OnPayload(t *testing.T) {
srv := newTestService(t)
// This test directly reads things from srv.messages that normally
// is read by internal goroutine started with Start(). So let's
// pretend we really did start already.
srv.started.Store(true)
priv, _ := getTestValidator(1)
p := new(Payload)
p.SetValidatorIndex(1)
p.SetPayload(&prepareRequest{})
p.encodeData()
// sender is invalid
require.NoError(t, srv.OnPayload(&p.Extensible))
shouldNotReceive(t, srv.messages)
p = new(Payload)
p.SetValidatorIndex(1)
p.Sender = priv.GetScriptHash()
p.SetPayload(&prepareRequest{})
require.NoError(t, p.Sign(priv))
require.NoError(t, srv.OnPayload(&p.Extensible))
shouldReceive(t, srv.messages)
}
func TestVerifyBlock(t *testing.T) {
srv := newTestService(t)
bc := srv.Chain.(*core.Blockchain)
srv.lastTimestamp = 1
t.Run("good empty", func(t *testing.T) {
b := testchain.NewBlock(t, bc, 1, 0)
require.True(t, srv.verifyBlock(&neoBlock{Block: *b}))
})
t.Run("good pooled tx", func(t *testing.T) {
tx := transaction.New([]byte{byte(opcode.RET)}, 100000)
tx.ValidUntilBlock = 1
addSender(t, tx)
signTx(t, srv.Chain, tx)
require.NoError(t, srv.Chain.PoolTx(tx))
b := testchain.NewBlock(t, bc, 1, 0, tx)
require.True(t, srv.verifyBlock(&neoBlock{Block: *b}))
})
t.Run("good non-pooled tx", func(t *testing.T) {
tx := transaction.New([]byte{byte(opcode.RET)}, 100000)
tx.ValidUntilBlock = 1
addSender(t, tx)
signTx(t, srv.Chain, tx)
b := testchain.NewBlock(t, bc, 1, 0, tx)
require.True(t, srv.verifyBlock(&neoBlock{Block: *b}))
})
t.Run("good conflicting tx", func(t *testing.T) {
initGAS := srv.Chain.GetConfig().InitialGASSupply
tx1 := transaction.New([]byte{byte(opcode.RET)}, 100000)
tx1.NetworkFee = int64(initGAS)/2 + 1
tx1.ValidUntilBlock = 1
addSender(t, tx1)
signTx(t, srv.Chain, tx1)
tx2 := transaction.New([]byte{byte(opcode.RET)}, 100000)
tx2.NetworkFee = int64(initGAS)/2 + 1
tx2.ValidUntilBlock = 1
addSender(t, tx2)
signTx(t, srv.Chain, tx2)
require.NoError(t, srv.Chain.PoolTx(tx1))
require.Error(t, srv.Chain.PoolTx(tx2))
b := testchain.NewBlock(t, bc, 1, 0, tx2)
require.True(t, srv.verifyBlock(&neoBlock{Block: *b}))
})
t.Run("bad old", func(t *testing.T) {
b := testchain.NewBlock(t, bc, 1, 0)
b.Index = srv.Chain.BlockHeight()
require.False(t, srv.verifyBlock(&neoBlock{Block: *b}))
})
t.Run("bad big size", func(t *testing.T) {
script := make([]byte, int(srv.ProtocolConfiguration.MaxBlockSize))
script[0] = byte(opcode.RET)
tx := transaction.New(script, 100000)
tx.ValidUntilBlock = 1
addSender(t, tx)
signTx(t, srv.Chain, tx)
b := testchain.NewBlock(t, bc, 1, 0, tx)
require.False(t, srv.verifyBlock(&neoBlock{Block: *b}))
})
t.Run("bad timestamp", func(t *testing.T) {
b := testchain.NewBlock(t, bc, 1, 0)
b.Timestamp = srv.lastTimestamp - 1
require.False(t, srv.verifyBlock(&neoBlock{Block: *b}))
})
t.Run("bad tx", func(t *testing.T) {
tx := transaction.New([]byte{byte(opcode.RET)}, 100000)
tx.ValidUntilBlock = 1
addSender(t, tx)
signTx(t, srv.Chain, tx)
tx.Scripts[0].InvocationScript[16] = ^tx.Scripts[0].InvocationScript[16]
b := testchain.NewBlock(t, bc, 1, 0, tx)
require.False(t, srv.verifyBlock(&neoBlock{Block: *b}))
})
t.Run("bad big sys fee", func(t *testing.T) {
txes := make([]*transaction.Transaction, 2)
for i := range txes {
txes[i] = transaction.New([]byte{byte(opcode.RET)}, srv.ProtocolConfiguration.MaxBlockSystemFee/2+1)
txes[i].ValidUntilBlock = 1
addSender(t, txes[i])
signTx(t, srv.Chain, txes[i])
}
b := testchain.NewBlock(t, bc, 1, 0, txes...)
require.False(t, srv.verifyBlock(&neoBlock{Block: *b}))
})
}
func shouldReceive(t *testing.T, ch chan Payload) {
select {
case <-ch:
default:
require.Fail(t, "missing expected message")
}
}
func shouldNotReceive(t *testing.T, ch chan Payload) {
select {
case <-ch:
require.Fail(t, "unexpected message receive")
default:
}
}
func newTestServiceWithState(t *testing.T, stateRootInHeader bool) *service {
return newTestServiceWithChain(t, newTestChain(t, stateRootInHeader))
}
func newTestService(t *testing.T) *service {
return newTestServiceWithState(t, false)
}
func newTestServiceWithChain(t *testing.T, bc *core.Blockchain) *service {
srv, err := NewService(Config{
Logger: zaptest.NewLogger(t),
Broadcast: func(*npayload.Extensible) {},
Chain: bc,
ProtocolConfiguration: bc.GetConfig(),
RequestTx: func(...util.Uint256) {},
TimePerBlock: time.Duration(bc.GetConfig().SecondsPerBlock) * time.Second,
Wallet: &config.Wallet{
Path: "./testdata/wallet1.json",
Password: "one",
},
})
require.NoError(t, err)
return srv.(*service)
}
func getTestValidator(i int) (*privateKey, *publicKey) {
key := testchain.PrivateKey(i)
return &privateKey{PrivateKey: key}, &publicKey{PublicKey: key.PublicKey()}
}
func newSingleTestChain(t *testing.T) *core.Blockchain {
configPath := "../../config/protocol.unit_testnet.single.yml"
cfg, err := config.LoadFile(configPath)
require.NoError(t, err, "could not load config")
chain, err := core.NewBlockchain(storage.NewMemoryStore(), cfg.ProtocolConfiguration, zaptest.NewLogger(t))
require.NoError(t, err, "could not create chain")
go chain.Run()
t.Cleanup(chain.Close)
return chain
}
func newTestChain(t *testing.T, stateRootInHeader bool) *core.Blockchain {
unitTestNetCfg, err := config.Load("../../config", netmode.UnitTestNet)
require.NoError(t, err)
unitTestNetCfg.ProtocolConfiguration.StateRootInHeader = stateRootInHeader
chain, err := core.NewBlockchain(storage.NewMemoryStore(), unitTestNetCfg.ProtocolConfiguration, zaptest.NewLogger(t))
require.NoError(t, err)
go chain.Run()
t.Cleanup(chain.Close)
return chain
}
var neoOwner = testchain.MultisigScriptHash()
func addSender(t *testing.T, txs ...*transaction.Transaction) {
for _, tx := range txs {
tx.Signers = []transaction.Signer{
{
Account: neoOwner,
},
}
}
}
func signTx(t *testing.T, bc Ledger, txs ...*transaction.Transaction) {
validators := make([]*keys.PublicKey, 4)
privNetKeys := make([]*keys.PrivateKey, 4)
for i := 0; i < 4; i++ {
privNetKeys[i] = testchain.PrivateKey(i)
validators[i] = privNetKeys[i].PublicKey()
}
privNetKeys = privNetKeys[:3]
rawScript, err := smartcontract.CreateMultiSigRedeemScript(3, validators)
require.NoError(t, err)
for _, tx := range txs {
size := io.GetVarSize(tx)
netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), rawScript)
tx.NetworkFee += +netFee
size += sizeDelta
tx.NetworkFee += int64(size) * bc.FeePerByte()
buf := io.NewBufBinWriter()
for _, key := range privNetKeys {
signature := key.SignHashable(uint32(testchain.Network()), tx)
emit.Bytes(buf.BinWriter, signature)
}
tx.Scripts = []transaction.Witness{{
InvocationScript: buf.Bytes(),
VerificationScript: rawScript,
}}
}
}