native: fix Policy's IsBlocked behaviour

Account is blocked when it's in the Policy's storage, not when it's
missing from the Policy storage. Introduced in
bbbc6805a8.

This bug leads to the fact that during native Neo cache initialization
at the last block in the dBFT epoch, all candidates accounts are
"blocked", and thus, stand-by committee and validators are used in the
subsequent new epoch. Close #3424.

This bug may lead to the consequences described in #3273, but it needs
to be confirmed.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
This commit is contained in:
Anna Shaleva 2024-05-17 11:45:15 +03:00
parent 2d4993a837
commit c43cfae24c
2 changed files with 88 additions and 1 deletions

View file

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -316,6 +317,92 @@ func TestBlockchain_InitializeNeoCache_Bug3181(t *testing.T) {
}) })
} }
// TestBlockchain_InitializeNeoCache_Bug3424 ensures that Neo cache (new epoch
// committee and stand by validators) is properly initialized after node restart
// at the dBFT epoch boundary.
func TestBlockchain_InitializeNeoCache_Bug3424(t *testing.T) {
ps, path := newLevelDBForTestingWithPath(t, "")
bc, validators, committee, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, nil, ps)
require.NoError(t, err)
go bc.Run()
e := neotest.NewExecutor(t, bc, validators, committee)
cfg := e.Chain.GetConfig()
committeeSize := cfg.GetCommitteeSize(0)
validatorsCount := cfg.GetNumOfCNs(0)
// Stand by committee drives the chain.
standBySorted, err := keys.NewPublicKeysFromStrings(e.Chain.GetConfig().StandbyCommittee)
require.NoError(t, err)
standBySorted = standBySorted[:validatorsCount]
sort.Sort(standBySorted)
pubs := e.Chain.ComputeNextBlockValidators()
require.Equal(t, standBySorted, keys.PublicKeys(pubs))
// Move from stand by committee to the elected nodes.
e.ValidatorInvoker(e.NativeHash(t, nativenames.Gas)).Invoke(t, true, "transfer", e.Validator.ScriptHash(), e.CommitteeHash, 100_0000_0000, nil)
neoCommitteeInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Neo))
neoValidatorsInvoker := neoCommitteeInvoker.WithSigners(neoCommitteeInvoker.Validator)
policyInvoker := neoCommitteeInvoker.CommitteeInvoker(neoCommitteeInvoker.NativeHash(t, nativenames.Policy))
advanceChain := func(t *testing.T) {
for int(e.Chain.BlockHeight())%committeeSize != 0 {
neoCommitteeInvoker.AddNewBlock(t)
}
}
advanceChainToEpochBoundary := func(t *testing.T) {
for int(e.Chain.BlockHeight()+1)%committeeSize != 0 {
neoCommitteeInvoker.AddNewBlock(t)
}
}
// voters vote for candidates.
voters := make([]neotest.Signer, committeeSize+1)
candidates := make([]neotest.Signer, committeeSize+1)
for i := 0; i < committeeSize+1; i++ {
voters[i] = e.NewAccount(t, 10_0000_0000)
candidates[i] = e.NewAccount(t, 2000_0000_0000) // enough for one registration
}
txes := make([]*transaction.Transaction, 0, committeeSize*3)
for i := 0; i < committeeSize+1; i++ {
transferTx := neoValidatorsInvoker.PrepareInvoke(t, "transfer", e.Validator.ScriptHash(), voters[i].(neotest.SingleSigner).Account().PrivateKey().GetScriptHash(), int64(committeeSize+1-i)*1000000, nil)
txes = append(txes, transferTx)
registerTx := neoValidatorsInvoker.WithSigners(candidates[i]).PrepareInvoke(t, "registerCandidate", candidates[i].(neotest.SingleSigner).Account().PublicKey().Bytes())
txes = append(txes, registerTx)
voteTx := neoValidatorsInvoker.WithSigners(voters[i]).PrepareInvoke(t, "vote", voters[i].(neotest.SingleSigner).Account().PrivateKey().GetScriptHash(), candidates[i].(neotest.SingleSigner).Account().PublicKey().Bytes())
txes = append(txes, voteTx)
}
txes = append(txes, policyInvoker.PrepareInvoke(t, "blockAccount", candidates[len(candidates)-1].(neotest.SingleSigner).Account().ScriptHash()))
neoValidatorsInvoker.AddNewBlock(t, txes...)
for _, tx := range txes {
e.CheckHalt(t, tx.Hash(), stackitem.Make(true)) // luckily, both `transfer`, `registerCandidate` and `vote` return boolean values
}
// Ensure validators are properly updated.
advanceChain(t)
pubs = e.Chain.ComputeNextBlockValidators()
sortedCandidates := make(keys.PublicKeys, validatorsCount)
for i := range candidates[:validatorsCount] {
sortedCandidates[i] = candidates[i].(neotest.SingleSigner).Account().PublicKey()
}
sort.Sort(sortedCandidates)
require.EqualValues(t, sortedCandidates, keys.PublicKeys(pubs))
// Move to the last block in the epoch and restart the node.
advanceChainToEpochBoundary(t)
bc.Close() // Ensure persist is done and persistent store is properly closed.
ps, _ = newLevelDBForTestingWithPath(t, path)
t.Cleanup(func() { require.NoError(t, ps.Close()) })
// Start chain from the existing database that should trigger an update of native
// Neo newEpoch* cached values during initializaition. This update requires candidates
// list recalculation and caldidates policies checks.
bc, _, _, err = chain.NewMultiWithCustomConfigAndStoreNoCheck(t, nil, ps)
require.NoError(t, err)
pubs = bc.ComputeNextBlockValidators()
require.EqualValues(t, sortedCandidates, keys.PublicKeys(pubs))
}
// This test enables Notary native contract at non-zero height and checks that no // This test enables Notary native contract at non-zero height and checks that no
// Notary cache initialization is performed before that height on node restart. // Notary cache initialization is performed before that height on node restart.
/* /*

View file

@ -319,7 +319,7 @@ func (p *Policy) IsBlocked(dao *dao.Simple, hash util.Uint160) bool {
cache := dao.GetROCache(p.ID) cache := dao.GetROCache(p.ID)
if cache == nil { if cache == nil {
key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...) key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...)
return dao.GetStorageItem(p.ID, key) == nil return dao.GetStorageItem(p.ID, key) != nil
} }
_, isBlocked := p.isBlockedInternal(cache.(*PolicyCache), hash) _, isBlocked := p.isBlockedInternal(cache.(*PolicyCache), hash)
return isBlocked return isBlocked