Skip to content

Commit 76fff18

Browse files
authored
tests: fix assertions in emulator (#1043)
This PR fixes assertions checks in emulator and re-organises the validation logic a bit to facilitate testing. Signed-off-by: Jakub Sztandera <[email protected]>
1 parent a055c66 commit 76fff18

File tree

5 files changed

+98
-78
lines changed

5 files changed

+98
-78
lines changed

emulator/driver_assertions.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,11 @@ func (d *Driver) RequirePrepareAtRound(round uint64, value *gpbft.ECChain, justi
6060
d.require.NotNil(msg)
6161
instance := d.host.getInstance(msg.Vote.Instance)
6262
d.require.NotNil(instance)
63-
d.require.Equal(gpbft.PREPARE_PHASE, msg.Vote.Phase)
64-
d.require.Equal(round, msg.Vote.Round)
65-
d.require.True(value.Eq(msg.Vote.Value))
66-
d.require.Equal(instance.id, msg.Vote.Instance)
67-
d.require.Equal(instance.supplementalData, msg.Vote.SupplementalData)
63+
d.require.Equal(gpbft.PREPARE_PHASE, msg.Vote.Phase, "phase different: expected %s, got %s", gpbft.PREPARE_PHASE, msg.Vote.Phase)
64+
d.require.Equal(round, msg.Vote.Round, "round different")
65+
d.require.True(value.Eq(msg.Vote.Value), "value different")
66+
d.require.Equal(instance.id, msg.Vote.Instance, "instance different")
67+
d.require.Equal(instance.supplementalData, msg.Vote.SupplementalData, "supplemental data different")
6868
d.requireEqualJustification(justification, msg.Justification)
6969
d.require.Empty(msg.Ticket)
7070

@@ -85,7 +85,7 @@ func (d *Driver) RequireCommit(round uint64, vote *gpbft.ECChain, justification
8585
d.require.Equal(instance.supplementalData, msg.Vote.SupplementalData)
8686
d.require.Equal(instance.id, msg.Vote.Instance)
8787
d.require.True(vote.Eq(msg.Vote.Value))
88-
d.requireEqualJustification(justification, justification)
88+
d.requireEqualJustification(justification, msg.Justification)
8989
d.require.Empty(msg.Ticket)
9090

9191
d.require.NoError(d.deliverMessage(msg))
@@ -107,13 +107,22 @@ func (d *Driver) RequireConverge(round uint64, vote *gpbft.ECChain, justificatio
107107
d.require.NoError(d.deliverMessage(msg))
108108
}
109109

110+
// Must panics if err is non-nil, otherwise returns v.
111+
func Must[V any](v V, err error) V {
112+
if err != nil {
113+
panic(err)
114+
}
115+
return v
116+
}
117+
110118
func (d *Driver) requireEqualJustification(one *gpbft.Justification, other *gpbft.Justification) {
111119
if one == nil || other == nil {
112120
d.require.Equal(one, other)
113121
} else {
114-
d.require.Equal(one.Signature, other.Signature)
115-
d.require.Equal(one.Signers, other.Signers)
122+
d.require.Equal(Must(one.Signers.All(100000)), Must(other.Signers.All(100000)), "signers different")
123+
d.require.EqualExportedValues(one.Vote, other.Vote)
116124
d.require.True(one.Vote.Eq(&other.Vote))
125+
d.require.Equal(one.Signature, other.Signature, "signature different")
117126
}
118127
}
119128

gpbft/gpbft_test.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ func TestGPBFT_WithEvenPowerDistribution(t *testing.T) {
225225
Vote: instance.NewCommit(0, &gpbft.ECChain{}),
226226
})
227227

228-
evidenceOfCommitForBottom := instance.NewJustification(0, gpbft.COMMIT_PHASE, &gpbft.ECChain{}, 0, 1)
228+
evidenceOfCommitForBottom := instance.NewJustification(0, gpbft.COMMIT_PHASE, nil, 0, 1)
229229

230230
driver.RequireConverge(1, baseChain, evidenceOfCommitForBottom)
231231
driver.RequireDeliverMessage(&gpbft.GMessage{
@@ -342,7 +342,7 @@ func TestGPBFT_WithEvenPowerDistribution(t *testing.T) {
342342
driver.RequireStartInstance(futureInstance.ID())
343343
driver.RequireQuality()
344344
driver.RequirePrepare(futureInstance.Proposal())
345-
driver.RequireCommit(0, futureInstance.Proposal(), instance.NewJustification(0, gpbft.PREPARE_PHASE, futureInstance.Proposal(), 0, 1))
345+
driver.RequireCommit(0, futureInstance.Proposal(), futureInstance.NewJustification(0, gpbft.PREPARE_PHASE, futureInstance.Proposal(), 0, 1))
346346
})
347347

348348
t.Run("Rebroadcasts selected messages on timeout", func(t *testing.T) {
@@ -379,7 +379,7 @@ func TestGPBFT_WithEvenPowerDistribution(t *testing.T) {
379379
})
380380

381381
// Expect progress to COMMIT with strong evidence of PREPARE.
382-
evidenceOfPrepare := instance.NewJustification(0, gpbft.PREPARE_PHASE, instance.Proposal(), 0, 1)
382+
evidenceOfPrepare := instance.NewJustification(0, gpbft.PREPARE_PHASE, baseChain, 0, 1)
383383
driver.RequireCommit(0, baseChain, evidenceOfPrepare)
384384

385385
// Expect no messages until the rebroadcast timeout has expired.
@@ -463,6 +463,7 @@ func TestGPBFT_WithEvenPowerDistribution(t *testing.T) {
463463

464464
futureRoundProposal := instance.Proposal().Extend(tipSet4.Key)
465465
evidenceOfPrepareAtRound76 := instance.NewJustification(76, gpbft.PREPARE_PHASE, futureRoundProposal, 0, 1)
466+
evidenceOfPrepareAtRound77 := instance.NewJustification(77, gpbft.PREPARE_PHASE, futureRoundProposal, 0, 1)
466467

467468
// Send Prepare messages to facilitate weak quorum of prepare at future round.
468469
driver.RequireDeliverMessage(&gpbft.GMessage{
@@ -480,7 +481,7 @@ func TestGPBFT_WithEvenPowerDistribution(t *testing.T) {
480481
// Expect skip to round.
481482
driver.RequireConverge(77, futureRoundProposal, evidenceOfPrepareAtRound76)
482483
driver.RequirePrepareAtRound(77, futureRoundProposal, evidenceOfPrepareAtRound76)
483-
driver.RequireCommit(77, futureRoundProposal, evidenceOfPrepareAtRound76)
484+
driver.RequireCommit(77, futureRoundProposal, evidenceOfPrepareAtRound77)
484485
// Expect no messages until the rebroadcast timeout has expired.
485486
driver.RequireNoBroadcast()
486487
// Trigger rebroadcast alarm.
@@ -492,7 +493,7 @@ func TestGPBFT_WithEvenPowerDistribution(t *testing.T) {
492493
//
493494
// See: https://github.com/filecoin-project/go-f3/issues/595
494495
driver.RequireQuality()
495-
driver.RequireCommit(77, futureRoundProposal, evidenceOfPrepareAtRound76)
496+
driver.RequireCommit(77, futureRoundProposal, evidenceOfPrepareAtRound77)
496497
driver.RequirePrepareAtRound(77, futureRoundProposal, evidenceOfPrepareAtRound76)
497498
driver.RequireConverge(77, futureRoundProposal, evidenceOfPrepareAtRound76)
498499
driver.RequireNoBroadcast()
@@ -501,7 +502,7 @@ func TestGPBFT_WithEvenPowerDistribution(t *testing.T) {
501502
driver.RequireDeliverMessage(&gpbft.GMessage{
502503
Sender: 1,
503504
Vote: instance.NewCommit(77, futureRoundProposal),
504-
Justification: evidenceOfPrepareAtRound76,
505+
Justification: evidenceOfPrepareAtRound77,
505506
})
506507

507508
// Expect DECIDE with strong evidence of COMMIT.
@@ -651,7 +652,7 @@ func TestGPBFT_WithEvenPowerDistribution(t *testing.T) {
651652
Vote: instance.NewPrepare(0, instance.Proposal().BaseChain()),
652653
})
653654
// Assert COMMIT phase for base decision.
654-
driver.RequireCommit(0, instance.Proposal().BaseChain(), instance.NewJustification(0, gpbft.PREPARE_PHASE, instance.Proposal(), 0, 1))
655+
driver.RequireCommit(0, instance.Proposal().BaseChain(), instance.NewJustification(0, gpbft.PREPARE_PHASE, instance.Proposal().BaseChain(), 0, 1))
655656
}
656657
t.Run("Justification of CONVERGE to bottom from next round completes phase", func(t *testing.T) {
657658
instance, driver := newInstanceAndDriver(t)
@@ -783,7 +784,7 @@ func TestGPBFT_WithExactOneThirdToTwoThirdPowerDistribution(t *testing.T) {
783784
Vote: instance.NewPrepare(0, baseChain),
784785
},
785786
)
786-
driver.RequireCommit(0, baseChain, instance.NewJustification(0, gpbft.PREPARE_PHASE, baseChain, 1, 0))
787+
driver.RequireCommit(0, baseChain, instance.NewJustification(0, gpbft.PREPARE_PHASE, baseChain, 1))
787788

788789
// Trigger timeout of COMMIT phase to force a scheduled re-broadcast.
789790
driver.RequireDeliverAlarm()

gpbft/participant_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -992,7 +992,7 @@ func TestParticipant_ValidateMessage(t *testing.T) {
992992
wantErr: "has justification from wrong round",
993993
},
994994
{
995-
name: "justification with invalid value is error",
995+
name: "justification with different value is error",
996996
msg: func(subject *participantTestSubject) *gpbft.GMessage {
997997
subject.mockValidSignature(somePowerEntry.PubKey, signature)
998998
return &gpbft.GMessage{
@@ -1007,13 +1007,14 @@ func TestParticipant_ValidateMessage(t *testing.T) {
10071007
Justification: &gpbft.Justification{
10081008
Vote: gpbft.Payload{
10091009
Instance: initialInstanceNumber,
1010+
Phase: gpbft.PREPARE_PHASE,
10101011
Value: &gpbft.ECChain{TipSets: []*gpbft.TipSet{subject.canonicalChain.Base(), {PowerTable: subject.supplementalData.PowerTable}}},
10111012
SupplementalData: *subject.supplementalData,
10121013
},
10131014
},
10141015
}
10151016
},
1016-
wantErr: "invalid justification vote value chain",
1017+
wantErr: "has invalid justification vote value chain",
10171018
},
10181019
}
10191020
for _, test := range tests {

gpbft/validator.go

Lines changed: 69 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -338,39 +338,24 @@ func (v *cachingValidator) validateJustification(ctx context.Context, valueKey *
338338
partial := valueKey != nil
339339
cacheNamespace := validationNamespaces.justification(partial)
340340

341-
// It doesn't matter whether the justification is partial or not. Because, namespace
342-
// separates the two.
343-
cacheKey, err := v.getCacheKey(msg.Justification)
344-
var alreadyValidated bool
345-
if err != nil {
346-
log.Warnw("failed to get cache key for justification", "partial", partial, "err", err)
347-
// If we can't compute the cache key, we can't cache the justification. But we
348-
// can still validate it.
349-
cacheKey = nil
350-
} else {
351-
// Only cache the justification if:
352-
// * marshalling it was successful, and
353-
// * it is not yet present in the cache.
354-
if alreadyValidated, err = v.isAlreadyValidated(msg.Vote.Instance, cacheNamespace, cacheKey); err != nil {
355-
log.Warnw("failed to check if justification is already cached", "partial", partial, "err", err)
356-
} else if alreadyValidated {
357-
metrics.validationCache.Add(ctx, 1, metric.WithAttributes(attrCacheHit, attrCacheKindJustification, attrPartial(partial)))
358-
return nil
359-
} else {
360-
metrics.validationCache.Add(ctx, 1, metric.WithAttributes(attrCacheMiss, attrCacheKindJustification, attrPartial(partial)))
361-
}
362-
}
363-
364341
// Check that the justification is for the same instance.
365342
if msg.Vote.Instance != msg.Justification.Vote.Instance {
366343
return fmt.Errorf("message with instanceID %v has evidence from instanceID: %v", msg.Vote.Instance, msg.Justification.Vote.Instance)
367344
}
368345
if !msg.Vote.SupplementalData.Eq(&msg.Justification.Vote.SupplementalData) {
369346
return fmt.Errorf("message and justification have inconsistent supplemental data: %v != %v", msg.Vote.SupplementalData, msg.Justification.Vote.SupplementalData)
370347
}
348+
371349
// Check that justification vote value is a valid chain.
372350
if err := msg.Justification.Vote.Value.Validate(); err != nil {
373-
return fmt.Errorf("invalid justification vote value chain: %w", err)
351+
return fmt.Errorf("has invalid justification vote value chain: %w", err)
352+
}
353+
354+
zeroKey := (&ECChain{}).Key()
355+
msgKey := valueKey
356+
if !partial {
357+
key := msg.Vote.Value.Key()
358+
msgKey = &key
374359
}
375360

376361
// Check every remaining field of the justification, according to the phase requirements.
@@ -379,27 +364,27 @@ func (v *cachingValidator) validateJustification(ctx context.Context, valueKey *
379364
// Anything else is disallowed.
380365
expectations := map[Phase]map[Phase]struct {
381366
Round uint64
382-
Value *ECChain
367+
Key *ECChainKey
383368
}{
384369
// CONVERGE is justified by a strong quorum of COMMIT for bottom,
385370
// or a strong quorum of PREPARE for the same value, from the previous round.
386371
CONVERGE_PHASE: {
387-
COMMIT_PHASE: {msg.Vote.Round - 1, &ECChain{}},
388-
PREPARE_PHASE: {msg.Vote.Round - 1, msg.Vote.Value},
372+
COMMIT_PHASE: {msg.Vote.Round - 1, &zeroKey},
373+
PREPARE_PHASE: {msg.Vote.Round - 1, msgKey},
389374
},
390375
// PREPARE is justified by the same rules as CONVERGE (in rounds > 0).
391376
PREPARE_PHASE: {
392-
COMMIT_PHASE: {msg.Vote.Round - 1, &ECChain{}},
393-
PREPARE_PHASE: {msg.Vote.Round - 1, msg.Vote.Value},
377+
COMMIT_PHASE: {msg.Vote.Round - 1, &zeroKey},
378+
PREPARE_PHASE: {msg.Vote.Round - 1, msgKey},
394379
},
395380
// COMMIT is justified by a strong quorum of PREPARE from the same round with the same value.
396381
COMMIT_PHASE: {
397-
PREPARE_PHASE: {msg.Vote.Round, msg.Vote.Value},
382+
PREPARE_PHASE: {msg.Vote.Round, msgKey},
398383
},
399384
// DECIDE is justified by a strong quorum of COMMIT with the same value.
400385
// The DECIDE message doesn't specify a round.
401386
DECIDE_PHASE: {
402-
COMMIT_PHASE: {math.MaxUint64, msg.Vote.Value},
387+
COMMIT_PHASE: {math.MaxUint64, msgKey},
403388
},
404389
}
405390

@@ -410,25 +395,15 @@ func (v *cachingValidator) validateJustification(ctx context.Context, valueKey *
410395
return fmt.Errorf("message %v has justification from wrong round %d", msg, msg.Justification.Vote.Round)
411396
}
412397

413-
// There are 4 possible cases:
414-
// 1. The justification is from a complete message with a non-zero value
415-
// 2. The justification is from a complete message with a zero value
416-
// 3. The justification is from a partial message with non-zero value key
417-
// 4. The justification is from a partial message with zero value key
418-
//
419-
// In cases 1 and 2, the justification vote value must match the expected value
420-
// exactly.
421-
//
422-
// Whereas in cases 3 and 4, the justification vote can't directly be checked and
423-
// instead we rely on asserting the value via signature verification. Because the
424-
// signing payload uses the value key only.
425-
if partial {
426-
expectedVoteValueKey = *valueKey
427-
} else {
428-
if !msg.Justification.Vote.Value.Eq(expected.Value) {
398+
// The key can be either the value key or the zero key.
399+
// Depending on which type of justification we are dealing with,
400+
// if message is not partial, then check the justification Value is same as the expected Value
401+
expectedVoteValueKey = *expected.Key
402+
if !partial {
403+
justificationVoteValueKey := msg.Justification.Vote.Value.Key()
404+
if !bytes.Equal(justificationVoteValueKey[:], expected.Key[:]) {
429405
return fmt.Errorf("message %v has justification for a different value: %v", msg, msg.Justification.Vote.Value)
430406
}
431-
expectedVoteValueKey = expected.Value.Key()
432407
}
433408
} else {
434409
return fmt.Errorf("message %v has justification with unexpected phase: %v", msg, msg.Justification.Vote.Phase)
@@ -437,18 +412,30 @@ func (v *cachingValidator) validateJustification(ctx context.Context, valueKey *
437412
return fmt.Errorf("message %v has unexpected phase for justification", msg)
438413
}
439414

440-
// Check justification power and signature.
441-
justificationPower, signers, err := msg.Justification.GetSigners(comt.PowerTable)
415+
cacheKey, err := v.getCacheKey(msg.Justification, expectedVoteValueKey[:])
416+
var alreadyValidated bool
442417
if err != nil {
443-
return fmt.Errorf("failed to get justification signers: %w", err)
444-
}
445-
if !IsStrongQuorum(justificationPower, comt.PowerTable.ScaledTotal) {
446-
return fmt.Errorf("message %v has justification with insufficient power: %v :%w", msg, justificationPower, ErrValidationInvalid)
418+
log.Warnw("failed to get cache key for justification", "partial", partial, "err", err)
419+
// If we can't compute the cache key, we can't cache the justification. But we
420+
// can still validate it.
421+
cacheKey = nil
422+
} else {
423+
// Only cache the justification if:
424+
// * marshalling it was successful, and
425+
// * it is not yet present in the cache.
426+
if alreadyValidated, err = v.isAlreadyValidated(msg.Vote.Instance, cacheNamespace, cacheKey); err != nil {
427+
log.Warnw("failed to check if justification is already cached", "partial", partial, "err", err)
428+
} else if alreadyValidated {
429+
metrics.validationCache.Add(ctx, 1, metric.WithAttributes(attrCacheHit, attrCacheKindJustification, attrPartial(partial)))
430+
return nil
431+
} else {
432+
metrics.validationCache.Add(ctx, 1, metric.WithAttributes(attrCacheMiss, attrCacheKindJustification, attrPartial(partial)))
433+
}
447434
}
448435

449-
payload := msg.Justification.Vote.MarshalForSigningWithValueKey(v.networkName, expectedVoteValueKey)
450-
if err := comt.AggregateVerifier.VerifyAggregate(signers, payload, msg.Justification.Signature); err != nil {
451-
return fmt.Errorf("verification of the aggregate failed: %+v: %w", msg.Justification, err)
436+
err = v.validateJustificationSignature(comt, msg.Justification, expectedVoteValueKey)
437+
if err != nil {
438+
return fmt.Errorf("internal justification validation failed: %w", err)
452439
}
453440

454441
if len(cacheKey) > 0 {
@@ -461,6 +448,27 @@ func (v *cachingValidator) validateJustification(ctx context.Context, valueKey *
461448
return nil
462449
}
463450

451+
func (v *cachingValidator) validateJustificationSignature(comt *Committee, justif *Justification, expectedVoteValueKey ECChainKey) error {
452+
// It doesn't matter whether the justification is partial or not. Because, namespace
453+
// separates the two.
454+
455+
// Check justification power and signature.
456+
justificationPower, signers, err := justif.GetSigners(comt.PowerTable)
457+
if err != nil {
458+
return fmt.Errorf("failed to get justification signers: %w", err)
459+
}
460+
if !IsStrongQuorum(justificationPower, comt.PowerTable.ScaledTotal) {
461+
return fmt.Errorf("has justification with insufficient power: %v :%w", justificationPower, ErrValidationInvalid)
462+
}
463+
464+
payload := justif.Vote.MarshalForSigningWithValueKey(v.networkName, expectedVoteValueKey)
465+
if err := comt.AggregateVerifier.VerifyAggregate(signers, payload, justif.Signature); err != nil {
466+
return fmt.Errorf("verification of the aggregate failed: %+v: %w", justif, err)
467+
}
468+
469+
return nil
470+
}
471+
464472
func (v *cachingValidator) isAlreadyValidated(group uint64, namespace validatorNamespace, cacheKey []byte) (bool, error) {
465473
alreadyValidated, err := v.cache.Contains(group, namespace, cacheKey)
466474
if err != nil {
@@ -469,10 +477,13 @@ func (v *cachingValidator) isAlreadyValidated(group uint64, namespace validatorN
469477
return alreadyValidated, nil
470478
}
471479

472-
func (v *cachingValidator) getCacheKey(msg cbor.Marshaler) ([]byte, error) {
480+
func (v *cachingValidator) getCacheKey(msg cbor.Marshaler, additionalFields ...[]byte) ([]byte, error) {
473481
var buf bytes.Buffer
474482
if err := msg.MarshalCBOR(&buf); err != nil {
475483
return nil, fmt.Errorf("failed to get cache key: %w", err)
476484
}
485+
for _, field := range additionalFields {
486+
_, _ = buf.Write(field)
487+
}
477488
return buf.Bytes(), nil
478489
}

merkle/merkle_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import (
1313
func TestHashTree(t *testing.T) {
1414
for i := 1; i < 256; i++ {
1515
t.Run(fmt.Sprintf("Length/%d", i), func(t *testing.T) {
16-
t.Parallel()
17-
1816
test := make([][]byte, i)
1917
for j := range test {
2018
test[j] = []byte{byte(j)}

0 commit comments

Comments
 (0)