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
38 changes: 38 additions & 0 deletions app/lep6_module_order_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package app

import (
"testing"

actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types"
audittypes "github.com/LumeraProtocol/lumera/x/audit/v1/types"
supernodetypes "github.com/LumeraProtocol/lumera/x/supernode/v1/types"
"github.com/stretchr/testify/require"
)

func TestLEP6ModuleOrderingPinsSupernodeAuditAction(t *testing.T) {
assertOrdered := func(t *testing.T, name string, modules []string) {
t.Helper()
supernodeIdx := indexOfModule(modules, supernodetypes.ModuleName)
auditIdx := indexOfModule(modules, audittypes.ModuleName)
actionIdx := indexOfModule(modules, actiontypes.ModuleName)

require.NotEqual(t, -1, supernodeIdx, "%s missing supernode module", name)
require.NotEqual(t, -1, auditIdx, "%s missing audit module", name)
require.NotEqual(t, -1, actionIdx, "%s missing action module", name)
require.Less(t, supernodeIdx, auditIdx, "%s must run supernode before audit for LEP-6 dependency ordering", name)
require.Less(t, auditIdx, actionIdx, "%s must run audit before action so action finalization can anchor LEP-6 artifact counts", name)
}

assertOrdered(t, "genesisModuleOrder", genesisModuleOrder)
assertOrdered(t, "beginBlockers", beginBlockers)
assertOrdered(t, "endBlockers", endBlockers)
}

func indexOfModule(modules []string, target string) int {
for i, module := range modules {
if module == target {
return i
}
}
return -1
}
124 changes: 110 additions & 14 deletions docs/leps/LEP-6-implementation-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This guide documents the `lumera` implementation of LEP-6 storage-truth enforcem

Priority design source: `/home/openclaw/workspace/docs/LEP6.md`

Branch: `LEP-6-consensus-gap-fixes-rebase` @ `5df4206` (rebased onto post-#118 `LEP-6-foundation`)
Branch: `LEP-6-foundation-review-r3` (R3 hardening branch; 20/20 Zee R3 findings addressed locally, pending push)

## Reviewer Summary

Expand Down Expand Up @@ -117,7 +117,7 @@ Business rules:

- Non-empty proof results require `INDEX` or `SYMBOL`.
- `NO_ELIGIBLE_TICKET` requires `UNSPECIFIED`.
- Index failures are treated as Class A faults and also satisfy strong-postpone and heal-eligibility predicates.
- Class A is result-class driven: `HASH_MISMATCH` (with INDEX vs SYMBOL magnitude) and `RECHECK_CONFIRMED_FAIL`. `ArtifactClass == INDEX` alone does **not** make `TIMEOUT_OR_NO_RESPONSE` a Class A fault.

### Result Enum Values

Expand Down Expand Up @@ -1383,7 +1383,7 @@ Behavior deltas applied on top of `LEP-6-foundation` tip `868cbc7c` to close
24 production-gate findings. Branch `LEP-6-foundation-review-fixes` (squashed
single commit `0c6f5f0`). Test pyramid green at `0c6f5f0`: unit + module
simulation + `tests/integration/` + `tests/system/` + `tests/systemtests/`
(`-tags=system_test`, 25/25 PASS). Compare:
(`-tags=system_test`, 25/25 PASS). Final pushed SHA was `df15913` after docs-only amend. Compare:
[`LEP-6-foundation...LEP-6-foundation-review-fixes`](https://github.com/LumeraProtocol/lumera/compare/LEP-6-foundation...LEP-6-foundation-review-fixes).

### HIGH (consensus / state-correctness / money-flow)
Expand Down Expand Up @@ -1523,6 +1523,98 @@ below) and codified as Skill Pitfall #31 in the

---

## LEP-6 Round-3 Review Hardening (Zee R3 — PR #117 review 4188900358)

Behavior deltas applied on top of the post-R2 `LEP-6-foundation` tip `8748065`
to close 20 additional production-gate findings (3 HIGH / 3 MEDIUM / 14 LOW).
Branch `LEP-6-foundation-review-r3` is a single-commit delivery branch. Test
pyramid green before push: targeted R3 tests, `go build ./...`, audit/action/app
unit packages, `go test ./x/...`, action+audit integration tests,
`go test -tags=system ./tests/system/...`, and full e2e systemtests
`go test -tags=system_test -timeout=1800s -v .` with
`ok github.com/LumeraProtocol/lumera/tests/systemtests 1031.464s`.
Compare after push:
[`LEP-6-foundation...LEP-6-foundation-review-r3`](https://github.com/LumeraProtocol/lumera/compare/LEP-6-foundation...LEP-6-foundation-review-r3).

### HIGH (state-correctness / production safety)

- **B-F1 — reporter clean-pass reward scans all result classes.**
`storageTruthReporterEpochPassStats` now counts PASS results while detecting
overturned failure-class records across the epoch, so a reporter does not earn
the per-epoch `-4` reliability reward when any failure was overturned by
recheck.
- **C-F1 — strong-recovery clean-pass param is validated.**
`Params.Validate()` rejects `StorageTruthStrongRecoveryCleanPassCount <= 0`
and values below `StorageTruthRecoveryCleanPassCount`, preventing impossible
strong-postpone recovery semantics.
- **C-F3 — retention migration covers every LEP-6 lookback window.**
`requiredHistory` and the v1→v2 migration include
`StorageTruthDivergenceWindowEpochs` and `StorageTruthHealDeadlineEpochs`, so
pruning cannot erase evidence still needed by divergence or heal-deadline
logic.

### MEDIUM (semantic correctness / sibling symmetry)

- **C-F2 — heal verifier count bounded.**
`StorageTruthHealVerifierCount` is constrained to `1..32`, preventing
unbounded verifier loops or invalid zero-quorum behavior.
- **B-F2 — Class-A predicate tightened to result class.**
`TIMEOUT_OR_NO_RESPONSE` on an INDEX artifact remains Class B and unscaled;
only `HASH_MISMATCH` / `RECHECK_CONFIRMED_FAIL` enter Class-A scoring paths.
This corrected the prior R2 approximation that treated `ArtifactClass==INDEX`
alone as Class A.
- **C-F4 — action test fixture exercises reward routing as a no-op.**
`ActionKeeperWithAddress` now uses `MockRewardDistributionKeeper{Bps:0}`
rather than nil, so tests do not bypass the production fee-routing branch.

### LOW (defensive hardening / genesis closure / review guardrails)

- **C-F5 — postpone thresholds are strictly ordered.** Equality between
postpone and strong-postpone thresholds is rejected.
- **A-F1 — heal-op ID counter recovery matches evidence ID recovery.**
Malformed/missing/zero counters no longer panic or risk reuse; the next ID is
derived from existing heal ops.
- **A-F2 / B-F3 — MaxUint64-safe epoch scans.** Named range helpers avoid
`endEpoch+1` overflow for reporter-result and transcript scans.
- **A-F4 — genesis validates `TicketArtifactCountState`.** Empty ticket IDs and
all-zero counts are rejected.
- **A-F5 — transcript genesis import rejects unknown/trailing JSON fields.**
Prevents silent import drift.
- **A-F6 — genesis round-trip now seeds and verifies non-empty node-failure and
reporter-result facts.**
- **B-F4 — failed-heal marker setter errors on empty healer/ticket and callers
propagate the error.** No more silent no-op state holes.
- **B-F5 — clean-epoch recovery creates state for fresh reporters with PASS
facts.** Dashboards/reliability state no longer undercount new reporters.
- **B-F6 — cross-holder PASS bonus coverage spans non-hash prior failures.**
TIMEOUT, OBSERVER_QUORUM_FAIL, and INVALID_TRANSCRIPT prior classes are now
covered, not just HASH_MISMATCH.
- **C-F6 — strict artifact-count fallback.** Explicit metadata counts win;
legacy metadata falls back to `len(RqIdsIds)`; if both are absent the path
returns an error instead of silently anchoring `(0,0)`.
- **C-F7 — module order pinned by app-level test.** Genesis/begin/end ordering
must remain `supernode → audit → action`.
- **C-F9 — migration-position closure.** Audit consensus version remains `2`;
R3 backfills are folded into `NewMigrateV1ToV2` because v2 has not shipped.

### Why R2 missed these (process retrospective)

R3 contained no outright reviewer flip-flops. The misses fell into three buckets:
(1) **refinements** of R2 approximations (Class-A predicate and heal-op counter
recovery), (2) **incomplete R2 implementation** where new params or promoted
rules lacked a validation bound, and (3) **latent sibling-symmetry/genesis
closure items** discovered by a deeper production-gate pass. Going forward,
review closure requires not only the R2 sweeps but also MaxUint64 boundary tests,
genesis unknown-field rejection, strict fallback behavior for legacy metadata,
and app-level module-order pinning.

### No new params introduced this round

R3 added validation and migration coverage for existing R2/R3 state surfaces but
introduced no additional governance params or protobuf fields.

---

## Pre-Release Checklist

This section is the canonical aggregator of every operational, follow-up,
Expand Down Expand Up @@ -1577,7 +1669,8 @@ Source-of-truth references: `ACTIVE_WORK.md` (in-flight tracking),
### C. Review-process sweeps (mandatory before each release-gate PR merge)

These three sweeps were missed in round 1 and produced the 24 R2
findings. They are now mandatory pre-master gates per Skill Pitfall #31.
findings; R3 added sibling-boundary checks for MaxUint64 ranges, strict
fallbacks, and genesis import hardening. They are mandatory pre-master gates.

- [ ] **Out-of-scope diff sweep** — `git diff <release-base>..HEAD --stat`
filtered to files outside the announced scope; flag any deletion or
Expand Down Expand Up @@ -1616,9 +1709,11 @@ findings. They are now mandatory pre-master gates per Skill Pitfall #31.
7. Cycle to recovery (clean passes); verify postponement→active
transition and (for strong postpone) the new
`StorageTruthStrongRecoveryCleanPassCount` gate.
8. Verify R2 deltas land as designed: a cross-holder PASS produces
8. Verify R2/R3 deltas land as designed: a cross-holder PASS produces
`D -= 3` extra; per-epoch PASS reward is single `-4` (not
per-result); EXPIRED heal-op advances probation and bumps `D`.
per-result); EXPIRED heal-op advances probation and bumps `D`;
`TIMEOUT_OR_NO_RESPONSE` on INDEX remains Class B; legacy action metadata
with neither explicit counts nor `RqIdsIds` errors instead of anchoring `(0,0)`.

### E. Test pyramid re-validation at activation tag

Expand All @@ -1628,7 +1723,7 @@ findings. They are now mandatory pre-master gates per Skill Pitfall #31.
- [ ] `./tests/integration/...` green.
- [ ] `./tests/system/...` (`-tags=system`) green.
- [ ] `./tests/systemtests/...` (`-tags=system_test`) green
(25/25 last verified at `0c6f5f0`).
(last verified for R3 at `LEP-6-foundation-review-r3`: `ok .../tests/systemtests 1031.464s`).
- [ ] Determinism scan clean — `grep -rE 'float|math\.Pow|time\.Now|rand\.|sort\.Float|FormatFloat'`
on `x/audit/v1/keeper` returns zero hits in consensus paths; no
`range map[]` in scoring/divergence/enforcement consensus paths.
Expand All @@ -1644,18 +1739,19 @@ findings. They are now mandatory pre-master gates per Skill Pitfall #31.
gas-requirements note (above) in operator docs. Include in the
SOFT/FULL activation proposal body so all participants see it before
voting.
- [ ] **Operator changelog** — publish the R2 behavior deltas (new params,
- [ ] **Operator changelog** — publish the R2/R3 behavior deltas (new params,
per-epoch PASS reward semantics, EXPIRED heal-op cooldown, strong-band
recovery threshold, cross-holder PASS bonus) in the release notes so
recovery threshold, cross-holder PASS bonus, Class-A predicate tightening,
strict artifact-count fallback) in the release notes so
operators understand observable score-evolution changes.

### G. Documentation queue close-out

- [ ] `.lep6-review-pending-doc-updates/CP1_TRIAGE.md` and
`CP2_SPEC_ALIGNMENT.md` items resolved or explicitly deferred with
rationale.
- [ ] `.lep6-review-pending-doc-updates/r2/` items reflected in this
guide (this section) and in `workspace/docs/LEP6.md` where the spec
text needed correction (NF7 done; sweep for any further drift).
- [ ] `docs/agent-context/02_lumera.md` updated with R2 behavior deltas
and new params for cross-session continuity.
- [ ] `.lep6-review-pending-doc-updates/r2/` and R3 review items reflected in this
guide and in `workspace/docs/LEP6.md` where the spec text needed correction
(NF7 done; sweep for any further drift).
- [ ] `docs/agent-context/02_lumera.md` updated with R2/R3 behavior deltas
and new params/validation hardening for cross-session continuity.
2 changes: 1 addition & 1 deletion testutil/keeper/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ func ActionKeeperWithAddress(t testing.TB, ctrl *gomock.Controller, accounts []A
func() *ibckeeper.Keeper {
return ibckeeper.NewKeeper(encCfg.Codec, storeService, newMockIbcParams(), mockUpgradeKeeper, authority.String())
},
nil,
&MockRewardDistributionKeeper{Bps: 0}, // Per CP-R3 C-F4 — exercise reward-routing branch as no-op, not nil short-circuit.
)

// Initialize params
Expand Down
5 changes: 4 additions & 1 deletion x/action/v1/keeper/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,10 @@ func (k *Keeper) FinalizeAction(ctx sdk.Context, actionID string, superNodeAccou
if err := gogoproto.Unmarshal(existingAction.Metadata, &cascadeMeta); err != nil {
return errors.Wrap(actiontypes.ErrInvalidMetadata, fmt.Sprintf("failed to unmarshal finalized cascade metadata: %v", err))
}
indexCount, symbolCount := actiontypes.CascadeArtifactCountsWithFallback(&cascadeMeta)
indexCount, symbolCount, err := actiontypes.CascadeArtifactCountsWithFallbackStrict(&cascadeMeta)
if err != nil {
return errors.Wrap(actiontypes.ErrInvalidMetadata, err.Error())
}
if err := k.auditKeeper.SetStorageTruthTicketArtifactCounts(
ctx,
existingAction.ActionID,
Expand Down
14 changes: 10 additions & 4 deletions x/action/v1/keeper/action_cascade.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,11 @@ func (h CascadeActionHandler) Process(metadataBytes []byte, msgType common.Messa
// Backward-compatible fallback for finalize payloads that do not yet
// provide explicit LEP-6 artifact counts (single-source-of-truth via
// CascadeArtifactCountsWithFallback per CP-NEW-C-2 / 122-F2).
metadata.IndexArtifactCount, metadata.SymbolArtifactCount =
actiontypes.CascadeArtifactCountsWithFallback(&metadata)
indexCount, symbolCount, err := actiontypes.CascadeArtifactCountsWithFallbackStrict(&metadata)
if err != nil {
return nil, err
}
metadata.IndexArtifactCount, metadata.SymbolArtifactCount = indexCount, symbolCount
default:
return nil, fmt.Errorf("unsupported message type: %s", msgType)
}
Expand Down Expand Up @@ -323,8 +326,11 @@ func (h CascadeActionHandler) GetUpdatedMetadata(ctx sdk.Context, existingMetada
IndexArtifactCount: newMetadata.GetIndexArtifactCount(),
SymbolArtifactCount: newMetadata.GetSymbolArtifactCount(),
}
updatedMetadata.IndexArtifactCount, updatedMetadata.SymbolArtifactCount =
actiontypes.CascadeArtifactCountsWithFallback(updatedMetadata)
indexCount, symbolCount, err := actiontypes.CascadeArtifactCountsWithFallbackStrict(updatedMetadata)
if err != nil {
return nil, errors.Wrap(actiontypes.ErrInvalidMetadata, err.Error())
}
updatedMetadata.IndexArtifactCount, updatedMetadata.SymbolArtifactCount = indexCount, symbolCount

return gogoproto.Marshal(updatedMetadata)
}
21 changes: 18 additions & 3 deletions x/action/v1/types/metadata.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
package types

import "fmt"

// CascadeArtifactCountsWithFallback returns the (index, symbol) artifact
// counts from a CascadeMetadata, falling back to len(RqIdsIds) when either
// field is zero. This is the single-source-of-truth helper enforcing the
// 122-F2 fallback rule across all sites that consume cascade artifact
// counts (Process, GetUpdatedMetadata, FinalizeAction → audit hook).
//
// If meta is nil, returns (0, 0).
// If meta is nil, returns (0, 0). Callers that make consensus/state decisions
// must use CascadeArtifactCountsWithFallbackStrict so malformed metadata cannot
// silently resolve to a zero-count artifact universe.
func CascadeArtifactCountsWithFallback(meta *CascadeMetadata) (uint32, uint32) {
idx, sym, _ := CascadeArtifactCountsWithFallbackStrict(meta)
return idx, sym
}

// CascadeArtifactCountsWithFallbackStrict is the consensus-safe variant of
// CascadeArtifactCountsWithFallback. It rejects metadata where explicit counts
// are missing and RqIdsIds cannot provide the backward-compatible fallback.
func CascadeArtifactCountsWithFallbackStrict(meta *CascadeMetadata) (uint32, uint32, error) {
if meta == nil {
return 0, 0
return 0, 0, fmt.Errorf("cascade metadata is required")
}
idx := meta.GetIndexArtifactCount()
sym := meta.GetSymbolArtifactCount()
Expand All @@ -20,5 +32,8 @@ func CascadeArtifactCountsWithFallback(meta *CascadeMetadata) (uint32, uint32) {
if sym == 0 {
sym = fallback
}
return idx, sym
if idx == 0 || sym == 0 {
return 0, 0, fmt.Errorf("cascade artifact counts unavailable: explicit index/symbol counts missing and rq_ids_ids empty")
}
return idx, sym, nil
}
18 changes: 18 additions & 0 deletions x/action/v1/types/metadata_proto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,21 @@ func TestCascadeMetadataRoundTripWithNewFields(t *testing.T) {
require.NoError(t, proto.Unmarshal(bz, &decoded))
require.Equal(t, extended, &decoded)
}

func TestCascadeArtifactCountsWithFallbackStrictRejectsEmptyFallbackUniverse(t *testing.T) {
idx, sym, err := CascadeArtifactCountsWithFallbackStrict(&CascadeMetadata{})
require.Error(t, err)
require.Zero(t, idx)
require.Zero(t, sym)
require.Contains(t, err.Error(), "rq_ids_ids empty")

idx, sym, err = CascadeArtifactCountsWithFallbackStrict(&CascadeMetadata{RqIdsIds: []string{"rq-1", "rq-2"}})
require.NoError(t, err)
require.Equal(t, uint32(2), idx)
require.Equal(t, uint32(2), sym)

idx, sym, err = CascadeArtifactCountsWithFallbackStrict(&CascadeMetadata{IndexArtifactCount: 4, SymbolArtifactCount: 9})
require.NoError(t, err)
require.Equal(t, uint32(4), idx)
require.Equal(t, uint32(9), sym)
}
39 changes: 39 additions & 0 deletions x/audit/v1/keeper/export_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package keeper

import (
"encoding/json"

sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/LumeraProtocol/lumera/x/audit/v1/types"
Expand Down Expand Up @@ -30,3 +32,40 @@ var ApplyTicketDeteriorationDeltaForTest = func(k Keeper, ctx sdk.Context, epoch
var WriteRawNextHealOpIDForTest = func(k Keeper, ctx sdk.Context, raw []byte) {
k.kvStore(ctx).Set(types.NextHealOpIDKey(), raw)
}

// SetReporterResultOverturnFlagForTest writes a reporter-result record with the
// OverturnedByRecheck flag explicitly set. Used to verify CP-R3 B-F1 — the
// "no overturned fails" gate must inspect failure-class records, not PASS.
var SetReporterResultOverturnFlagForTest = func(k Keeper, ctx sdk.Context, epochID uint64, reporterAccount string, result *types.StorageProofResult, overturned bool) error {
if err := k.setStorageTruthReporterResult(ctx, epochID, reporterAccount, result); err != nil {
return err
}
store := k.kvStore(ctx)
primary := types.ReporterStorageTruthResultKey(reporterAccount, epochID, result.TicketId, result.TargetSupernodeAccount)
bz := store.Get(primary)
if bz == nil {
return nil
}
var rec storageTruthReporterResultRecord
if err := json.Unmarshal(bz, &rec); err != nil {
return err
}
rec.OverturnedByRecheck = overturned
updated, err := json.Marshal(rec)
if err != nil {
return err
}
store.Set(primary, updated)
store.Set(types.ReporterStorageTruthResultByTargetKey(result.TargetSupernodeAccount, epochID, result.TicketId, reporterAccount), updated)
return nil
}

// HasIndependentReporterPassInWindowForTest exposes the indexed lookup for CP-R3 B-F3 overflow coverage.
var HasIndependentReporterPassInWindowForTest = func(k Keeper, ctx sdk.Context, ticketID string, targetAccount string, excludeReporter string, startEpoch uint64, endEpoch uint64) (bool, error) {
return k.hasIndependentReporterPassInWindow(ctx, ticketID, targetAccount, excludeReporter, startEpoch, endEpoch)
}

// HasCleanRecheckInWindowForTest exposes the indexed lookup for CP-R3 B-F3 overflow coverage.
var HasCleanRecheckInWindowForTest = func(k Keeper, ctx sdk.Context, ticketID string, targetAccount string, startEpoch uint64, endEpoch uint64) (bool, error) {
return k.hasCleanRecheckInWindow(ctx, ticketID, targetAccount, startEpoch, endEpoch)
}
4 changes: 3 additions & 1 deletion x/audit/v1/keeper/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@ func (k Keeper) InitGenesis(ctx context.Context, genState types.GenesisState) er
k.importReporterResultFactForGenesis(sdkCtx, f)
}
for _, m := range genState.FailedHealMarkers {
k.setStorageTruthFailedHeal(sdkCtx, m.SupernodeAccount, m.EpochId, m.TicketId)
if err := k.setStorageTruthFailedHeal(sdkCtx, m.SupernodeAccount, m.EpochId, m.TicketId); err != nil {
return err
}
}
for _, r := range genState.EpochReports {
if err := k.SetReportRaw(sdkCtx, r); err != nil {
Expand Down
Loading
Loading