Skip to content

Commit 2862be8

Browse files
Fix empty standard block check (#3775)
Signed-off-by: Stephen Buttolph <stephen@avalabs.org> Co-authored-by: Arran Schlosberg <519948+ARR4N@users.noreply.github.com>
1 parent 7e56747 commit 2862be8

File tree

5 files changed

+103
-41
lines changed

5 files changed

+103
-41
lines changed

vms/platformvm/block/executor/standard_block_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ func TestBanffStandardBlockTimeVerification(t *testing.T) {
260260
require.NoError(err)
261261
block := env.blkManager.NewBlock(banffChildBlk)
262262
err = block.Verify(context.Background())
263-
require.ErrorIs(err, errBanffStandardBlockWithoutChanges)
263+
require.ErrorIs(err, ErrStandardBlockWithoutChanges)
264264
}
265265

266266
{

vms/platformvm/block/executor/verifier.go

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ import (
2727
var (
2828
_ block.Visitor = (*verifier)(nil)
2929

30-
ErrConflictingBlockTxs = errors.New("block contains conflicting transactions")
30+
ErrConflictingBlockTxs = errors.New("block contains conflicting transactions")
31+
ErrStandardBlockWithoutChanges = errors.New("BanffStandardBlock performs no state changes")
3132

3233
errApricotBlockIssuedAfterFork = errors.New("apricot block issued after fork")
33-
errBanffStandardBlockWithoutChanges = errors.New("BanffStandardBlock performs no state changes")
3434
errIncorrectBlockHeight = errors.New("incorrect block height")
3535
errOptionBlockTimestampNotMatchingParent = errors.New("option block proposed timestamp not matching parent block one")
3636
)
@@ -46,14 +46,14 @@ func (v *verifier) BanffAbortBlock(b *block.BanffAbortBlock) error {
4646
if err := v.banffOptionBlock(b); err != nil {
4747
return err
4848
}
49-
return v.abortBlock(b)
49+
return v.abortBlock(b) // Must be the last validity check on the block
5050
}
5151

5252
func (v *verifier) BanffCommitBlock(b *block.BanffCommitBlock) error {
5353
if err := v.banffOptionBlock(b); err != nil {
5454
return err
5555
}
56-
return v.commitBlock(b)
56+
return v.commitBlock(b) // Must be the last validity check on the block
5757
}
5858

5959
func (v *verifier) BanffProposalBlock(b *block.BanffProposalBlock) error {
@@ -94,7 +94,7 @@ func (v *verifier) BanffProposalBlock(b *block.BanffProposalBlock) error {
9494
return err
9595
}
9696

97-
return v.proposalBlock(
97+
return v.proposalBlock( // Must be the last validity check on the block
9898
b,
9999
b.Tx,
100100
onDecisionState,
@@ -130,41 +130,27 @@ func (v *verifier) BanffStandardBlock(b *block.BanffStandardBlock) error {
130130
}
131131

132132
feeCalculator := state.PickFeeCalculator(v.txExecutorBackend.Config, onAcceptState)
133-
lowBalanceL1ValidatorsEvicted, err := v.standardBlock(
133+
return v.standardBlock( // Must be the last validity check on the block
134134
b,
135135
b.Transactions,
136136
feeCalculator,
137137
onAcceptState,
138+
changed,
138139
)
139-
if err != nil {
140-
return err
141-
}
142-
143-
// Verify that the block performs changes. If it does not, it never should
144-
// have been issued. Prior to the F upgrade, evicting L1 validators that
145-
// don't have enough balance for the next second is not considered a change.
146-
// After the F upgrade, it is.
147-
if !changed &&
148-
len(b.Transactions) == 0 &&
149-
(!v.txExecutorBackend.Config.UpgradeConfig.IsFortunaActivated(b.Timestamp()) || !lowBalanceL1ValidatorsEvicted) {
150-
return errBanffStandardBlockWithoutChanges
151-
}
152-
153-
return nil
154140
}
155141

156142
func (v *verifier) ApricotAbortBlock(b *block.ApricotAbortBlock) error {
157143
if err := v.apricotCommonBlock(b); err != nil {
158144
return err
159145
}
160-
return v.abortBlock(b)
146+
return v.abortBlock(b) // Must be the last validity check on the block
161147
}
162148

163149
func (v *verifier) ApricotCommitBlock(b *block.ApricotCommitBlock) error {
164150
if err := v.apricotCommonBlock(b); err != nil {
165151
return err
166152
}
167-
return v.commitBlock(b)
153+
return v.commitBlock(b) // Must be the last validity check on the block
168154
}
169155

170156
func (v *verifier) ApricotProposalBlock(b *block.ApricotProposalBlock) error {
@@ -183,7 +169,7 @@ func (v *verifier) ApricotProposalBlock(b *block.ApricotProposalBlock) error {
183169
}
184170

185171
feeCalculator := txfee.NewSimpleCalculator(0)
186-
return v.proposalBlock(
172+
return v.proposalBlock( // Must be the last validity check on the block
187173
b,
188174
b.Tx,
189175
nil,
@@ -209,13 +195,13 @@ func (v *verifier) ApricotStandardBlock(b *block.ApricotStandardBlock) error {
209195
}
210196

211197
feeCalculator := txfee.NewSimpleCalculator(0)
212-
_, err = v.standardBlock(
198+
return v.standardBlock( // Must be the last validity check on the block
213199
b,
214200
b.Transactions,
215201
feeCalculator,
216202
onAcceptState,
203+
true,
217204
)
218-
return err
219205
}
220206

221207
func (v *verifier) ApricotAtomicBlock(b *block.ApricotAtomicBlock) error {
@@ -360,7 +346,10 @@ func (v *verifier) commonBlock(b block.Block) error {
360346
return nil
361347
}
362348

363-
// abortBlock populates the state of this block if [nil] is returned
349+
// abortBlock populates the state of this block if [nil] is returned.
350+
//
351+
// Invariant: The call to abortBlock must be the last validity check on the
352+
// block. If this function returns [nil], the block is cached as valid.
364353
func (v *verifier) abortBlock(b block.Block) error {
365354
parentID := b.Parent()
366355
onAbortState, ok := v.getOnAbortState(parentID)
@@ -384,7 +373,10 @@ func (v *verifier) abortBlock(b block.Block) error {
384373
return nil
385374
}
386375

387-
// commitBlock populates the state of this block if [nil] is returned
376+
// commitBlock populates the state of this block if [nil] is returned.
377+
//
378+
// Invariant: The call to commitBlock must be the last validity check on the
379+
// block. If this function returns [nil], the block is cached as valid.
388380
func (v *verifier) commitBlock(b block.Block) error {
389381
parentID := b.Parent()
390382
onCommitState, ok := v.getOnCommitState(parentID)
@@ -408,7 +400,10 @@ func (v *verifier) commitBlock(b block.Block) error {
408400
return nil
409401
}
410402

411-
// proposalBlock populates the state of this block if [nil] is returned
403+
// proposalBlock populates the state of this block if [nil] is returned.
404+
//
405+
// Invariant: The call to proposalBlock must be the last validity check on the
406+
// block. If this function returns [nil], the block is cached as valid.
412407
func (v *verifier) proposalBlock(
413408
b block.Block,
414409
tx *txs.Tx,
@@ -468,21 +463,35 @@ func (v *verifier) proposalBlock(
468463
return nil
469464
}
470465

471-
// standardBlock populates the state of this block if [nil] is returned
466+
// standardBlock populates the state of this block if [nil] is returned.
467+
//
468+
// Invariant: The call to standardBlock must be the last validity check on the
469+
// block. If this function returns [nil], the block is cached as valid.
472470
func (v *verifier) standardBlock(
473471
b block.Block,
474472
txs []*txs.Tx,
475473
feeCalculator txfee.Calculator,
476474
onAcceptState state.Diff,
477-
) (bool, error) {
475+
changedDuringAdvanceTime bool,
476+
) error {
478477
inputs, atomicRequests, onAcceptFunc, gasConsumed, lowBalanceL1ValidatorsEvicted, err := v.processStandardTxs(
479478
txs,
480479
feeCalculator,
481480
onAcceptState,
482481
b.Parent(),
483482
)
484483
if err != nil {
485-
return false, err
484+
return err
485+
}
486+
487+
// Verify that the block performs changes. If it does not, it never should
488+
// have been issued. Prior to Fortuna, evicting L1 validators that don't
489+
// have enough balance for the next second is not considered a change. After
490+
// Fortuna, it is.
491+
timestamp := onAcceptState.GetTimestamp()
492+
isFortuna := v.txExecutorBackend.Config.UpgradeConfig.IsFortunaActivated(timestamp)
493+
if hasChanges := changedDuringAdvanceTime || len(txs) > 0 || (isFortuna && lowBalanceL1ValidatorsEvicted); !hasChanges {
494+
return ErrStandardBlockWithoutChanges
486495
}
487496

488497
v.Mempool.Remove(txs...)
@@ -494,7 +503,7 @@ func (v *verifier) standardBlock(
494503
onAcceptState: onAcceptState,
495504
onAcceptFunc: onAcceptFunc,
496505

497-
timestamp: onAcceptState.GetTimestamp(),
506+
timestamp: timestamp,
498507
inputs: inputs,
499508
atomicRequests: atomicRequests,
500509
verifiedHeights: set.Of(v.pChainHeight),
@@ -505,7 +514,7 @@ func (v *verifier) standardBlock(
505514
gasConsumed,
506515
),
507516
}
508-
return lowBalanceL1ValidatorsEvicted, nil
517+
return nil
509518
}
510519

511520
func (v *verifier) processStandardTxs(txs []*txs.Tx, feeCalculator txfee.Calculator, diff state.Diff, parentID ids.ID) (

vms/platformvm/block/executor/verifier_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1349,19 +1349,19 @@ func TestDeactivateLowBalanceL1ValidatorBlockChanges(t *testing.T) {
13491349
name: "Before F Upgrade - no L1 validators evicted",
13501350
currentFork: upgradetest.Etna,
13511351
durationToAdvance: 0,
1352-
expectedErr: errBanffStandardBlockWithoutChanges,
1352+
expectedErr: ErrStandardBlockWithoutChanges,
13531353
},
13541354
{
13551355
name: "After F Upgrade - no L1 validators evicted",
13561356
currentFork: upgradetest.Fortuna,
13571357
durationToAdvance: 0,
1358-
expectedErr: errBanffStandardBlockWithoutChanges,
1358+
expectedErr: ErrStandardBlockWithoutChanges,
13591359
},
13601360
{
13611361
name: "Before F Upgrade - L1 validators evicted",
13621362
currentFork: upgradetest.Etna,
13631363
durationToAdvance: time.Second,
1364-
expectedErr: errBanffStandardBlockWithoutChanges,
1364+
expectedErr: ErrStandardBlockWithoutChanges,
13651365
},
13661366
{
13671367
name: "After F Upgrade - L1 validators evicted",

vms/platformvm/vm_regression_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2180,6 +2180,56 @@ func TestValidatorSetRaceCondition(t *testing.T) {
21802180
vm.ctx.Lock.Lock()
21812181
}
21822182

2183+
func TestL1ValidatorDeactivationCausesTrackingOfInvalidBlock(t *testing.T) {
2184+
require := require.New(t)
2185+
vm, _, _ := defaultVM(t, upgradetest.Etna)
2186+
vm.ctx.Lock.Lock()
2187+
defer vm.ctx.Lock.Unlock()
2188+
2189+
subnetID := testSubnet1.TxID
2190+
wallet := newWallet(t, vm, walletConfig{
2191+
subnetIDs: []ids.ID{subnetID},
2192+
})
2193+
2194+
nodeID := ids.GenerateTestNodeID()
2195+
sk, err := localsigner.New()
2196+
require.NoError(err)
2197+
pop, err := signer.NewProofOfPossession(sk)
2198+
require.NoError(err)
2199+
2200+
tx, err := wallet.IssueConvertSubnetToL1Tx(
2201+
subnetID,
2202+
ids.Empty,
2203+
nil,
2204+
[]*txs.ConvertSubnetToL1Validator{
2205+
{
2206+
NodeID: nodeID[:],
2207+
Weight: 1,
2208+
// Ensure that the validator is active for a 1 second and that
2209+
// it does not have sufficient balance to be active for 2
2210+
// seconds.
2211+
Balance: uint64(vm.ValidatorFeeConfig.MinPrice) + 1,
2212+
Signer: *pop,
2213+
},
2214+
},
2215+
)
2216+
require.NoError(err)
2217+
2218+
vm.ctx.Lock.Unlock()
2219+
require.NoError(vm.issueTxFromRPC(tx))
2220+
vm.ctx.Lock.Lock()
2221+
require.NoError(buildAndAcceptStandardBlock(vm))
2222+
2223+
vm.clock.Set(vm.clock.Time().Add(1 * time.Minute))
2224+
blk, err := vm.BuildBlock(context.Background())
2225+
require.NoError(err)
2226+
2227+
for range 2 {
2228+
err = blk.Verify(context.Background())
2229+
require.ErrorIs(err, blockexecutor.ErrStandardBlockWithoutChanges)
2230+
}
2231+
}
2232+
21832233
func buildAndAcceptStandardBlock(vm *VM) error {
21842234
blk, err := vm.Builder.BuildBlock(context.Background())
21852235
if err != nil {

vms/platformvm/vm_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,12 @@ var (
107107
ExcessConversionConstant: 5_000,
108108
}
109109
defaultValidatorFeeConfig = fee.Config{
110-
Capacity: 100,
111-
Target: 50,
112-
MinPrice: 1,
110+
Capacity: 100,
111+
Target: 50,
112+
// The minimum price is set to 2 so that tests can include cases where
113+
// L1 validator balances do not evenly divide into a timestamp granular
114+
// to a second.
115+
MinPrice: 2,
113116
ExcessConversionConstant: 100,
114117
}
115118

0 commit comments

Comments
 (0)