{"record":{"id":"6ae40c16c956c09c","repo":"canopy-network/canopy","slug":"target-round-d-must-be-greater-than-current-round","errorCode":null,"errorMessage":"target round %d must be greater than current round %d","messagePattern":"target round (.+?) must be greater than current round (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"bft/bft.go","lineNumber":635,"sourceCode":"\t\tif totalVotedPower > b.ValidatorSet.TotalPower/3 {\n\t\t\tpacemakerRound = vote.Qc.Header.Round // set the highest round where +1/3rds have been\n\t\t\tbreak\n\t\t}\n\t}\n\t// if +1/3rd Round is larger than local Round - advance to the +1/3rd Round to better join the Majority\n\tif pacemakerRound > b.Round {\n\t\tb.log.Infof(\"Pacemaker peers set round: %d\", pacemakerRound)\n\t\tb.Round = pacemakerRound\n\t\tb.round.Store(b.Round)\n\t}\n\treturn true\n}\n\n// ScheduleForceRound enters the exact target round at the first pacemaker\n// boundary at or after at. The caller must hold the Controller lock.\nfunc (b *BFT) ScheduleForceRound(round uint64, at time.Time, timeoutRound *uint64) error {\n\tif round <= b.Round {\n\t\treturn fmt.Errorf(\"target round %d must be greater than current round %d\", round, b.Round)\n\t}\n\tb.forcedRound = round\n\tb.forcedRoundAt = at\n\tb.forcedTimeoutRound = timeoutRound\n\tb.log.Warnf(\"Scheduled forced consensus round %d at %s\", round, at.Format(time.RFC3339Nano))\n\tif b.Phase == Pacemaker {\n\t\tb.SetWaitTimers(time.Until(at), 0)\n\t}\n\treturn nil\n}\n\n// PacemakerMessages is a collection of 'View' messages keyed by each Replica's public key\n// These messages help Replicas synchronize their Rounds more effectively during periods of instability or failure\ntype PacemakerMessages map[string]*Message // [ public_key_string ] -> View message\n\n// AddPacemakerMessage() adds the 'View' message to the list (keyed by public key string)\nfunc (b *BFT) AddPacemakerMessage(msg *Message) (err lib.ErrorI) {\n\tb.Controller.Lock()","sourceCodeStart":617,"sourceCodeEnd":653,"githubUrl":"https://github.com/canopy-network/canopy/blob/ee8197d91dd410f6592cb650a94c925ee6dc8bad/bft/bft.go#L617-L653","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Re-read the current round immediately before scheduling and use max(current+1, desired)","Hold the Controller lock while reading b.Round and scheduling so the check cannot race","Skip the call if round <= current round (it is a no-op by definition, not an error to fix)","If repeated, investigate why the node's round keeps advancing past the planned target"],"exampleFix":"// before\nerr := bft.ScheduleForceRound(42, time.Now().Add(time.Minute), nil)\n// after\nif round := bft.Round; targetRound <= round {\n    targetRound = round + 1\n}\nerr := bft.ScheduleForceRound(targetRound, time.Now().Add(time.Minute), nil)","handlingStrategy":"validation","validationCode":"if targetRound <= bft.Round {\n    return fmt.Errorf(\"refusing to force: target %d <= current %d\", targetRound, bft.Round)\n}","typeGuard":"func canForceRound(target, current uint64) bool { return target > current }","tryCatchPattern":"if err := bft.ScheduleForceRound(round, at, nil); err != nil {\n    if strings.Contains(err.Error(), \"must be greater than current round\") {\n        // already at/past target — treat as success\n        return nil\n    }\n    return err\n}","preventionTips":["Read b.Round under the Controller lock right before scheduling","Derive target round from a fresh state read, not a cached snapshot","Treat <= current round as a benign no-op"],"tags":["bft","consensus","round","validation"],"backgroundTag":"invalid-argument-value","analyzedSha":"ee8197d91dd410f6592cb650a94c925ee6dc8bad","analyzedAt":"2026-09-06T09:30:15.973Z","contentChangedAt":"2026-09-06T09:30:15.973Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}