Skip to content

Commit

Permalink
native: rework native NEO next block validators cache
Browse files Browse the repository at this point in the history
Blockchain passes his own pure unwrapped DAO to
(*Blockchain).ComputeNextBlockValidators which means that native
RW NEO cache structure stored inside this DAO can be modified by
anyone who uses exported ComputeNextBlockValidators Blockchain API,
and technically it's valid, and we should allow this, because it's
the only purpose of `validators` caching. However, at the same time
some RPC server is allowed to request a subsequent wrapped DAO for
some test invocation. It means that descendant wrapped DAO
eventually will request RW NEO cache and try to `Copy()`
the underlying's DAO cache which is in direct use of
ComputeNextBlockValidators. Here's the race:
ComputeNextBlockValidators called by Consensus service tries to
update cached `validators` value, and descendant wrapped DAO
created by the  RPC server tries to copy DAO's native cache and
read the cached `validators` value.

So the problem is that native cache not designated to handle
concurrent access between parent DAO layer and derived (wrapped)
DAO layer. I've carefully reviewed all the usages of native cache,
and turns out that the described situation is the only place where
parent DAO is used directly to modify its cache concurrently with
some descendant DAO that is trying to access the cache. All other
usages of native cache (not only NEO, but also all other native
contrcts) strictly rely on the hierarchical DAO structure and don't
ry to perform these concurrent operations between DAO layers.
There's also persist operation, but it keeps cache RW lock taken,
so it doesn't have this problem as far. Thus, in this commit we rework
NEO's `validators` cache value so that it always contain the relevant
list for upper Blockchain's DAO and is updated every PostPersist (if
needed).

Note: we must be very careful extending our native cache in the
future, every usage of native cache must be checked against the
described problem.

Close #2989.

Signed-off-by: Anna Shaleva <[email protected]>
  • Loading branch information
AnnaShaleva committed Aug 30, 2023
1 parent 304ccc1 commit 2794105
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 20 deletions.
2 changes: 1 addition & 1 deletion pkg/core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -2705,7 +2705,7 @@ func (bc *Blockchain) GetCommittee() (keys.PublicKeys, error) {

// GetValidators returns current validators.
func (bc *Blockchain) GetValidators() ([]*keys.PublicKey, error) {
return bc.contracts.NEO.ComputeNextBlockValidators(bc.blockHeight, bc.dao)
return bc.contracts.NEO.ComputeNextBlockValidators(bc.dao)
}

// GetNextBlockValidators returns next block validators.
Expand Down
72 changes: 53 additions & 19 deletions pkg/core/native/native_neo.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ type NeoCache struct {

votesChanged bool
nextValidators keys.PublicKeys
validators keys.PublicKeys
// validators contains cached next block validators. This list is updated every
// PostPersist if candidates votes ratio has been changed or register/unregister
// operation was performed within the persisted block. The updated value is
// being persisted following the standard layered DAO persist rules, so that
// external users will always get the proper value with upper Blockchain's DAO.
validators keys.PublicKeys
// committee contains cached committee members and their votes.
// It is updated once in a while depending on committee size
// (every 28 blocks for mainnet). It's value
Expand Down Expand Up @@ -269,6 +274,7 @@ func (n *NEO) Initialize(ic *interop.Context) error {
cache := &NeoCache{
gasPerVoteCache: make(map[string]big.Int),
votesChanged: true,
validators: nil, // will be updated in genezis PostPersist.
}

// We need cache to be present in DAO before the subsequent call to `mint`.
Expand Down Expand Up @@ -325,6 +331,12 @@ func (n *NEO) InitializeCache(blockHeight uint32, d *dao.Simple) error {
cache.gasPerBlock = n.getSortedGASRecordFromDAO(d)
cache.registerPrice = getIntWithKey(n.ID, d, []byte{prefixRegisterPrice})

numOfCNs := n.cfg.GetNumOfCNs(blockHeight + 1)
err := n.updateCachedValidators(d, cache, blockHeight, numOfCNs)
if err != nil {
return fmt.Errorf("failed to update next block validators cache: %w", err)
}

d.SetCache(n.ID, cache)
return nil
}
Expand Down Expand Up @@ -353,6 +365,20 @@ func (n *NEO) updateCache(cache *NeoCache, cvs keysWithVotes, blockHeight uint32
return nil
}

// updateCachedValidators sets validators cache that will be used by external users
// to retrieve next block validators list. Thus, it stores the list of validators
// computed using the currently persisted block state.
func (n *NEO) updateCachedValidators(d *dao.Simple, cache *NeoCache, blockHeight uint32, numOfCNs int) error {
result, _, err := n.computeCommitteeMembers(blockHeight, d)
if err != nil {
return fmt.Errorf("failed to compute committee members: %w", err)
}
result = result[:numOfCNs]
sort.Sort(result)
cache.validators = result
return nil
}

func (n *NEO) updateCommittee(cache *NeoCache, ic *interop.Context) error {
if !cache.votesChanged {
// We need to put in storage anyway, as it affects dumps
Expand Down Expand Up @@ -399,6 +425,7 @@ func (n *NEO) PostPersist(ic *interop.Context) error {
committeeReward := new(big.Int).Mul(gas, bigCommitteeRewardRatio)
n.GAS.mint(ic, pubs[index].GetScriptHash(), committeeReward.Div(committeeReward, big100), false)

var isCacheRW bool
if n.cfg.ShouldUpdateCommitteeAt(ic.Block.Index) {
var voterReward = new(big.Int).Set(bigVoterRewardRatio)
voterReward.Mul(voterReward, gas)
Expand All @@ -408,9 +435,8 @@ func (n *NEO) PostPersist(ic *interop.Context) error {
voterReward.Div(voterReward, big100)

var (
cs = cache.committee
isCacheRW bool
key = make([]byte, 34)
cs = cache.committee
key = make([]byte, 34)
)
for i := range cs {
if cs[i].Votes.Sign() > 0 {
Expand All @@ -437,6 +463,21 @@ func (n *NEO) PostPersist(ic *interop.Context) error {
}
}
}
// Update next block validators cache for external users.
var (
h = ic.Block.Index // consider persisting block as stored to get _next_ block validators
numOfCNs = n.cfg.GetNumOfCNs(h + 1)
)
if cache.validators == nil || numOfCNs != len(cache.validators) {
if !isCacheRW {
cache = ic.DAO.GetRWCache(n.ID).(*NeoCache)
}
err := n.updateCachedValidators(ic.DAO, cache, h, numOfCNs)
if err != nil {
return fmt.Errorf("failed to update next block validators cache: %w", err)
}
}

return nil
}

Expand Down Expand Up @@ -1042,24 +1083,17 @@ func (n *NEO) getAccountState(ic *interop.Context, args []stackitem.Item) stacki
return item
}

// ComputeNextBlockValidators returns an actual list of current validators.
func (n *NEO) ComputeNextBlockValidators(blockHeight uint32, d *dao.Simple) (keys.PublicKeys, error) {
numOfCNs := n.cfg.GetNumOfCNs(blockHeight + 1)
// Most of the time it should be OK with RO cache, thus try to retrieve
// validators without RW cache creation to avoid cached values copying.
// ComputeNextBlockValidators returns an actual list of current validators that is
// relevant for the latest persisted block.
func (n *NEO) ComputeNextBlockValidators(d *dao.Simple) (keys.PublicKeys, error) {
// It should always be OK with RO cache if using lower-layered DAO with proper
// cache set.
cache := d.GetROCache(n.ID).(*NeoCache)
if vals := cache.validators; vals != nil && numOfCNs == len(vals) {
if vals := cache.validators; vals != nil {
return vals.Copy(), nil
}
cache = d.GetRWCache(n.ID).(*NeoCache)
result, _, err := n.computeCommitteeMembers(blockHeight, d)
if err != nil {
return nil, err
}
result = result[:numOfCNs]
sort.Sort(result)
cache.validators = result
return result, nil
// It's a program error not to have the right value in lower cache.
panic(fmt.Errorf("unexpected validators cache content: %v", cache.validators))
}

func (n *NEO) getCommittee(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
Expand Down

0 comments on commit 2794105

Please sign in to comment.