Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions internal/aggregation/aggregate.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ func (e *unitCostEstimator) observe(duration time.Duration, units int) {

func aggregateFromSnapshot(snap *Snapshot, cache *xmss.PubKeyCache, deadline time.Time, shadowRates shadow.Rates, estimator *unitCostEstimator) ([]*types.SignedAggregatedAttestation, []store.PayloadKV, []store.AttestationDeleteKey, bool, groupSkips) {
skips := groupSkips{}
if snap == nil || cache == nil {
if snap == nil || cache == nil || snap.headState == nil {
return nil, nil, nil, false, skips
}
if estimator == nil {
Expand Down Expand Up @@ -248,15 +248,15 @@ func aggregateFromSnapshot(snap *Snapshot, cache *xmss.PubKeyCache, deadline tim

// Non-nil: orderedGroups already dropped roots without data.
attData := attestationDataForRoot(snap, dataRoot)
targetState := snap.targetStates[attData.Target.Root]
if targetState == nil {
// The vote's target checkpoint has no stored state here, so its
// signers' pubkeys cannot be resolved. Counted rather than dropped
// silently: when every group lands here the session produces
// nothing and looks idle.
skips.add(metrics.AggGroupSkipMissingTargetState)
return
}

// Signers are resolved against the head state's registry. The
// validator set is written once at genesis and never by the state
// transition, so every state on the chain carries the same registry
// and the head's is equivalent to the vote's target. Reading the
// target's own state instead cost a store lookup per data root and
// silently dropped the group whenever that state was absent — which
// on devnet-5 was every group, every slot.
registry := snap.headState.Validators

// Bound this pass to what fits the remaining session budget at the
// current per-unit estimate. Child proofs go in first (most coverage
Expand All @@ -266,8 +266,8 @@ func aggregateFromSnapshot(snap *Snapshot, cache *xmss.PubKeyCache, deadline tim
remaining := estimator.maxUnitsWithin(time.Until(deadline))

covered := make(map[uint64]bool)
selectChildProofs(newEntry, targetState, childProofsBuf, covered, cache, &remaining)
selectChildProofs(knownEntry, targetState, childProofsBuf, covered, cache, &remaining)
selectChildProofs(newEntry, snap.headState, childProofsBuf, covered, cache, &remaining)
selectChildProofs(knownEntry, snap.headState, childProofsBuf, covered, cache, &remaining)

if gossipEntry != nil && len(gossipEntry.Signatures) > 0 {
sortedSigs := make([]store.AttestationSignatureEntry, len(gossipEntry.Signatures))
Expand All @@ -283,7 +283,7 @@ func aggregateFromSnapshot(snap *Snapshot, cache *xmss.PubKeyCache, deadline tim
if covered[sigEntry.ValidatorID] {
continue
}
if sigEntry.ValidatorID >= uint64(len(targetState.Validators)) {
if sigEntry.ValidatorID >= uint64(len(registry)) {
continue
}

Expand All @@ -297,7 +297,7 @@ func aggregateFromSnapshot(snap *Snapshot, cache *xmss.PubKeyCache, deadline tim
}
defer xmss.FreeSignature(sigHandle)

pk, err := cache.Get(targetState.Validators[sigEntry.ValidatorID].AttestationPubkey)
pk, err := cache.Get(registry[sigEntry.ValidatorID].AttestationPubkey)
if err != nil {
continue
}
Expand Down
6 changes: 4 additions & 2 deletions internal/aggregation/aggregate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ func TestAggregationMessageBuildsRootAndSlot(t *testing.T) {

func aggregateTestSnapshot(slots ...uint64) *Snapshot {
snap := &Snapshot{
// SnapshotInputs never yields a nil head state; signer resolution reads
// its validator registry.
headState: &types.State{LatestFinalized: &types.Checkpoint{Slot: 0}},
attSigs: make(map[[32]byte]*store.AttestationDataEntry),
newEntries: make(map[[32]byte]*store.PayloadEntry),
knownEntries: make(map[[32]byte]*store.PayloadEntry),
targetStates: make(map[[32]byte]*types.State),
}
for i, slot := range slots {
var dr [32]byte
Expand All @@ -64,7 +66,7 @@ func aggregateTestSnapshot(slots ...uint64) *Snapshot {
Data: &types.AttestationData{
Slot: slot,
Head: &types.Checkpoint{},
Target: &types.Checkpoint{},
Target: &types.Checkpoint{Slot: slot},
Source: &types.Checkpoint{},
},
}
Expand Down
39 changes: 21 additions & 18 deletions internal/aggregation/skips_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,34 @@ import (
"github.com/geanlabs/gean/xmss"
)

// A session that drops every group must say so. Reporting produced=0 with no
// reason is indistinguishable from an idle aggregator, which is what hid a
// devnet-5 aggregator producing nothing for 355 consecutive slots.
func TestAggregateFromSnapshotCountsMissingTargetState(t *testing.T) {
// A target whose own state was never stored must no longer cost its group.
// The validator registry is written once at genesis and never by the state
// transition, so the head state resolves the same signers — this is the case
// that silently produced nothing for 355 consecutive slots on devnet-5.
func TestAggregateResolvesSignersWithoutTargetState(t *testing.T) {
target := &types.Checkpoint{Slot: 42, Root: rootByte(9)}
snap := &Snapshot{
headState: &types.State{LatestFinalized: &types.Checkpoint{Slot: 0}},
headState: &types.State{
LatestFinalized: &types.Checkpoint{Slot: 0},
Validators: make([]*types.Validator, 8),
},
attSigs: map[[32]byte]*store.AttestationDataEntry{
rootByte(1): {Data: &types.AttestationData{Slot: 42, Target: target}},
rootByte(2): {Data: &types.AttestationData{Slot: 43, Target: target}},
},
// targetStates deliberately empty: no state stored for the target root.
targetStates: map[[32]byte]*types.State{},
}

aggs, _, _, _, skips := aggregateFromSnapshot(snap, xmss.NewPubKeyCache(), time.Time{}, shadow.Rates{}, newUnitCostEstimator())
_, _, _, _, skips := aggregateFromSnapshot(snap, xmss.NewPubKeyCache(), time.Time{}, shadow.Rates{}, newUnitCostEstimator())

if len(aggs) != 0 {
t.Fatalf("aggregates=%d, want 0 when no target state is stored", len(aggs))
// No group may be dropped for a reason that no longer exists; these groups
// carry no signatures, so they fall out as too-few-signers instead.
if got := skips[metrics.AggGroupSkipTooFewSigners]; got != 2 {
t.Errorf("too_few_signers=%d, want 2 (groups reached signer selection)", got)
}
if got := skips[metrics.AggGroupSkipMissingTargetState]; got != 2 {
t.Errorf("missing_target_state skips=%d, want 2 — the drop must be counted, not silent", got)
}
if skips.total() != 2 {
t.Errorf("total skips=%d, want 2", skips.total())
for reason := range skips {
if reason == "missing_target_state" {
t.Errorf("group dropped for a missing target state; the head registry should have been used")
}
}
}

Expand Down Expand Up @@ -77,12 +80,12 @@ func TestGroupSkipsSummary(t *testing.T) {
}

skips := groupSkips{
metrics.AggGroupSkipTooFewSigners: 2,
metrics.AggGroupSkipMissingTargetState: 5,
metrics.AggGroupSkipTooFewSigners: 2,
metrics.AggGroupSkipTargetJustified: 5,
}
got := skips.summary()

for _, want := range []string{"missing_target_state=5", "too_few_signers=2"} {
for _, want := range []string{"target_justified=5", "too_few_signers=2"} {
if !strings.Contains(got, want) {
t.Errorf("summary %q missing %q", got, want)
}
Expand Down
14 changes: 0 additions & 14 deletions internal/aggregation/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ type Snapshot struct {
attSigs map[[32]byte]*store.AttestationDataEntry
newEntries map[[32]byte]*store.PayloadEntry
knownEntries map[[32]byte]*store.PayloadEntry
targetStates map[[32]byte]*types.State
}

func SnapshotInputs(s *store.ConsensusStore) *Snapshot {
Expand All @@ -27,7 +26,6 @@ func SnapshotInputs(s *store.ConsensusStore) *Snapshot {
attSigs: s.AttestationSignatures.Snapshot(),
newEntries: make(map[[32]byte]*store.PayloadEntry),
knownEntries: make(map[[32]byte]*store.PayloadEntry),
targetStates: make(map[[32]byte]*types.State),
}

dataRoots := make(map[[32]byte]bool)
Expand All @@ -45,18 +43,6 @@ func SnapshotInputs(s *store.ConsensusStore) *Snapshot {
}
}

for dr := range dataRoots {
attData := attestationDataForRoot(snap, dr)
if attData == nil {
continue
}
if _, ok := snap.targetStates[attData.Target.Root]; !ok {
if state := s.GetState(attData.Target.Root); state != nil {
snap.targetStates[attData.Target.Root] = state
}
}
}

return snap
}

Expand Down
3 changes: 0 additions & 3 deletions internal/aggregation/snapshot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,6 @@ func TestSnapshotInputsCapturesPayloadAndTargetState(t *testing.T) {
if snap.newEntries[dataRoot] == nil {
t.Fatal("new payload entry not captured")
}
if snap.targetStates[headRoot] == nil || snap.targetStates[headRoot].Slot != headState.Slot {
t.Fatal("target state not captured")
}
}

func TestSnapshotInputsReturnsNilWithoutWork(t *testing.T) {
Expand Down
10 changes: 4 additions & 6 deletions internal/metrics/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,13 @@ var aggregatorSkipReasons = []string{
// session that drops every group reports produced=0, which is otherwise
// indistinguishable from having nothing to aggregate.
const (
AggGroupSkipMissingTargetState = "missing_target_state"
AggGroupSkipTargetJustified = "target_justified"
AggGroupSkipTooFewSigners = "too_few_signers"
AggGroupSkipBudget = "budget"
AggGroupSkipError = "error"
AggGroupSkipTargetJustified = "target_justified"
AggGroupSkipTooFewSigners = "too_few_signers"
AggGroupSkipBudget = "budget"
AggGroupSkipError = "error"
)

var aggregationGroupSkipReasons = []string{
AggGroupSkipMissingTargetState,
AggGroupSkipTargetJustified,
AggGroupSkipTooFewSigners,
AggGroupSkipBudget,
Expand Down
Loading