canopy-network/canopy · warning

target round %d must be greater than current round %d

Error message

target round %d must be greater than current round %d

What it means

ScheduleForceRound refuses to schedule a forced round that is not strictly ahead of the controller's current round. It returns this error when round <= b.Round because forcing backwards or into the current round is an invalid view change.

Source

Thrown at bft/bft.go:635

		if totalVotedPower > b.ValidatorSet.TotalPower/3 {
			pacemakerRound = vote.Qc.Header.Round // set the highest round where +1/3rds have been
			break
		}
	}
	// if +1/3rd Round is larger than local Round - advance to the +1/3rd Round to better join the Majority
	if pacemakerRound > b.Round {
		b.log.Infof("Pacemaker peers set round: %d", pacemakerRound)
		b.Round = pacemakerRound
		b.round.Store(b.Round)
	}
	return true
}

// ScheduleForceRound enters the exact target round at the first pacemaker
// boundary at or after at. The caller must hold the Controller lock.
func (b *BFT) ScheduleForceRound(round uint64, at time.Time, timeoutRound *uint64) error {
	if round <= b.Round {
		return fmt.Errorf("target round %d must be greater than current round %d", round, b.Round)
	}
	b.forcedRound = round
	b.forcedRoundAt = at
	b.forcedTimeoutRound = timeoutRound
	b.log.Warnf("Scheduled forced consensus round %d at %s", round, at.Format(time.RFC3339Nano))
	if b.Phase == Pacemaker {
		b.SetWaitTimers(time.Until(at), 0)
	}
	return nil
}

// PacemakerMessages is a collection of 'View' messages keyed by each Replica's public key
// These messages help Replicas synchronize their Rounds more effectively during periods of instability or failure
type PacemakerMessages map[string]*Message // [ public_key_string ] -> View message

// AddPacemakerMessage() adds the 'View' message to the list (keyed by public key string)
func (b *BFT) AddPacemakerMessage(msg *Message) (err lib.ErrorI) {
	b.Controller.Lock()

View on GitHub (pinned to ee8197d91d)

Solutions

  1. Re-read the current round immediately before scheduling and use max(current+1, desired)
  2. Hold the Controller lock while reading b.Round and scheduling so the check cannot race
  3. Skip the call if round <= current round (it is a no-op by definition, not an error to fix)
  4. If repeated, investigate why the node's round keeps advancing past the planned target

Example fix

// before
err := bft.ScheduleForceRound(42, time.Now().Add(time.Minute), nil)
// after
if round := bft.Round; targetRound <= round {
    targetRound = round + 1
}
err := bft.ScheduleForceRound(targetRound, time.Now().Add(time.Minute), nil)
Defensive patterns

Strategy: validation

Validate before calling

if targetRound <= bft.Round {
    return fmt.Errorf("refusing to force: target %d <= current %d", targetRound, bft.Round)
}

Type guard

func canForceRound(target, current uint64) bool { return target > current }

Try / catch

if err := bft.ScheduleForceRound(round, at, nil); err != nil {
    if strings.Contains(err.Error(), "must be greater than current round") {
        // already at/past target — treat as success
        return nil
    }
    return err
}

Prevention

When it happens

Trigger: Calling BFT.ScheduleForceRound(round, at, timeoutRound) while holding the Controller lock with a target round equal to or below the node's current b.Round.

Common situations: An admin/operator tool computes the target round from a stale snapshot of the chain state while the node already advanced, or two force requests race so the second one targets a round that has since been reached.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of canopy-network/canopy@ee8197d91d (2026-09-06). Data as JSON: /api/errors/6ae40c16c956c09c. Report an issue: GitHub.