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
309 changes: 272 additions & 37 deletions docs/leps/LEP-6-implementation-guide.md

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions proto/lumera/audit/v1/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ message GenesisState {
// storage_truth_postponements records active per-supernode postponement markers
// exported/imported at genesis. Per 121-F7.
repeated StorageTruthPostponement storage_truth_postponements = 10 [(gogoproto.nullable) = false];

// Per NEW-C-1: round-trip every epoch-scoped audit prefix.
repeated GenesisRecheckEvidence recheck_evidence = 11 [(gogoproto.nullable) = false];
repeated GenesisStorageProofTranscript storage_proof_transcripts = 12 [(gogoproto.nullable) = false];
repeated GenesisNodeFailureFact node_failure_facts = 13 [(gogoproto.nullable) = false];
repeated GenesisReporterResultFact reporter_result_facts = 14 [(gogoproto.nullable) = false];
repeated GenesisFailedHealMarker failed_heal_markers = 15 [(gogoproto.nullable) = false];
repeated EpochReport epoch_reports = 16 [(gogoproto.nullable) = false];
repeated GenesisReportIndex report_indices = 17 [(gogoproto.nullable) = false];
repeated GenesisHostReportIndex host_report_indices = 18 [(gogoproto.nullable) = false];
repeated GenesisStorageChallengeIndex storage_challenge_indices = 19 [(gogoproto.nullable) = false];
}

// StorageTruthPostponement records a supernode's storage-truth postponement state
Expand All @@ -44,3 +55,64 @@ message StorageTruthPostponement {
string supernode_account = 1;
uint64 postponed_at_epoch_id = 2;
}

// GenesisRecheckEvidence — st/rce/ replay-protection key.
message GenesisRecheckEvidence {
uint64 epoch_id = 1;
string ticket_id = 2;
string creator_account = 3;
}

// GenesisStorageProofTranscript — st/spt/ value (JSON-encoded record).
// The opaque record_json blob is the keeper's storageProofTranscriptRecord
// JSON marshaling. InitGenesis re-writes via the existing setter so the
// st/spt-tbe/ secondary index is rebuilt deterministically.
message GenesisStorageProofTranscript {
string transcript_hash = 1;
bytes record_json = 2;
}

// GenesisNodeFailureFact — st/nf/ entry (JSON-encoded record).
message GenesisNodeFailureFact {
string supernode_account = 1;
uint64 epoch_id = 2;
string ticket_id = 3;
string reporter_account = 4;
bytes record_json = 5;
}

// GenesisReporterResultFact — st/rrs/ entry (JSON-encoded record).
// The st/rrs-tt/ secondary index is rebuilt by the existing setter.
message GenesisReporterResultFact {
string reporter_account = 1;
uint64 epoch_id = 2;
string ticket_id = 3;
string target_account = 4;
bytes record_json = 5;
}

// GenesisFailedHealMarker — st/fh/ marker.
message GenesisFailedHealMarker {
string supernode_account = 1;
uint64 epoch_id = 2;
string ticket_id = 3;
}

// GenesisReportIndex — ri/ index entry.
message GenesisReportIndex {
string reporter_supernode_account = 1;
uint64 epoch_id = 2;
}

// GenesisHostReportIndex — hr/ index entry.
message GenesisHostReportIndex {
string reporter_supernode_account = 1;
uint64 epoch_id = 2;
}

// GenesisStorageChallengeIndex — sc/ index entry.
message GenesisStorageChallengeIndex {
string supernode_account = 1;
uint64 epoch_id = 2;
string reporter_supernode_account = 3;
}
8 changes: 8 additions & 0 deletions proto/lumera/audit/v1/params.proto
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,12 @@ message Params {

// Reporter challenger ineligibility duration in epochs (default 7).
uint32 storage_truth_reporter_ineligible_duration_epochs = 49;

// Strong-band recovery clean-pass requirement (F121-F12, default 5).
uint32 storage_truth_strong_recovery_clean_pass_count = 50;

// Number of verifier supernodes assigned per heal-op (NEW-B-3, default 2).
// Verifiers cross-check the healer's recovery; making this a Param allows
// governance to tune redundancy if heal volume / failure rate shifts.
uint32 storage_truth_heal_verifier_count = 51;
}
94 changes: 94 additions & 0 deletions testutil/keeper/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ func (m *ActionBankKeeper) GetBalance(ctx context.Context, addr sdk.AccAddress,
return sdk.Coin{}
}

func (m *ActionBankKeeper) SendCoinsFromModuleToModule(ctx context.Context, senderModule, recipientModule string, amt sdk.Coins) error {
if _, ok := m.moduleBalances[senderModule]; ok {
m.moduleBalances[senderModule] = m.moduleBalances[senderModule].Sub(amt...)
}
if m.moduleBalances[recipientModule].IsZero() {
m.moduleBalances[recipientModule] = amt
} else {
m.moduleBalances[recipientModule] = m.moduleBalances[recipientModule].Add(amt...)
}
return nil
}

func (m *ActionBankKeeper) GetModuleBalance(module string) sdk.Coins {
if coins, ok := m.moduleBalances[module]; ok {
return coins
Expand Down Expand Up @@ -213,6 +225,87 @@ func ActionKeeper(t testing.TB, ctrl *gomock.Controller) (keeper.Keeper, sdk.Con
return ActionKeeperWithAddress(t, ctrl, nil)
}

// MockRewardDistributionKeeper is a simple stub that returns a configurable bps value.
type MockRewardDistributionKeeper struct {
Bps uint64
}

func (m *MockRewardDistributionKeeper) GetRegistrationFeeShareBps(_ sdk.Context) uint64 {
return m.Bps
}

// ActionKeeperWithRewardDistribution returns an action keeper wired with the supplied
// RewardDistributionKeeper plus the bank-keeper mock so tests can assert module-balance
// deltas across the reward-share + foundation + supernode payout splits.
func ActionKeeperWithRewardDistribution(
t testing.TB,
ctrl *gomock.Controller,
accounts []AccountPair,
rewardDistKeeper actiontypes.RewardDistributionKeeper,
) (keeper.Keeper, sdk.Context, *ActionBankKeeper) {
storeKey := storetypes.NewKVStoreKey(actiontypes.StoreKey)

db := dbm.NewMemDB()
encCfg := moduletestutil.MakeTestEncodingConfig(actionmodulev1.AppModule{})
stateStore := store.NewCommitMultiStore(db, log.NewNopLogger(), metrics.NewNoOpMetrics())
stateStore.MountStoreWithDB(storeKey, storetypes.StoreTypeIAVL, db)
require.NoError(t, stateStore.LoadLatestVersion())

registry := codectypes.NewInterfaceRegistry()
cdc := codec.NewProtoCodec(registry)
authority := authtypes.NewModuleAddress(govtypes.ModuleName)

bankKeeper := NewActionMockBankKeeper()
authKeeper := NewMockAccountKeeper()
stakingKeeper := new(MockStakingKeeper)
supernodeKeeper := supernodemocks.NewMockSupernodeKeeper(ctrl)
supernodeQueryServer := supernodemocks.NewMockQueryServer(ctrl)
distributionKeeper := new(MockDistributionKeeper)
auditKeeper := NewMockAuditKeeper()

ctx := sdk.NewContext(stateStore, cmtproto.Header{}, false, log.NewNopLogger())
for _, acc := range accounts {
account := authKeeper.NewAccountWithAddress(ctx, acc.Address)
err := account.SetPubKey(acc.PubKey)
require.NoError(t, err)
authKeeper.SetAccount(ctx, account)
bankKeeper.sentCoins[acc.Address.String()] = sdk.NewCoins(sdk.NewInt64Coin("ulume", TestAccountAmount))
}

mockUpgradeKeeper := newMockUpgradeKeeper()

storeService := runtime.NewKVStoreService(storeKey)
k := keeper.NewKeeper(
cdc,
authKeeper.AddressCodec(),
storeService,
log.NewNopLogger(),
authority,
bankKeeper,
authKeeper,
stakingKeeper,
distributionKeeper,
supernodeKeeper,
func() sntypes.QueryServer {
return supernodeQueryServer
},
auditKeeper,
func() *ibckeeper.Keeper {
return ibckeeper.NewKeeper(encCfg.Codec, storeService, newMockIbcParams(), mockUpgradeKeeper, authority.String())
},
rewardDistKeeper,
)

params := actiontypes.DefaultParams()
params.FoundationFeeShare = "0.1"
params.SuperNodeFeeShare = "0.9"
if err := k.SetParams(ctx, params); err != nil {
panic(err)
}

return k, ctx, bankKeeper
}

func ActionKeeperWithAddress(t testing.TB, ctrl *gomock.Controller, accounts []AccountPair) (keeper.Keeper, sdk.Context) {
storeKey := storetypes.NewKVStoreKey(actiontypes.StoreKey)

Expand Down Expand Up @@ -272,6 +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,
)

// Initialize params
Expand Down
21 changes: 19 additions & 2 deletions x/action/v1/keeper/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,11 +250,12 @@ 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)
if err := k.auditKeeper.SetStorageTruthTicketArtifactCounts(
ctx,
existingAction.ActionID,
cascadeMeta.GetIndexArtifactCount(),
cascadeMeta.GetSymbolArtifactCount(),
indexCount,
symbolCount,
); err != nil {
return errors.Wrap(actiontypes.ErrInvalidMetadata, err.Error())
}
Expand Down Expand Up @@ -646,6 +647,22 @@ func (k *Keeper) DistributeFees(ctx sdk.Context, actionID string) error {
return nil // No supernodes to pay
}

// Route the configured reward-distribution share to the supernode-owned pool.
if k.rewardDistributionKeeper != nil {
rewardDistributionBps := k.rewardDistributionKeeper.GetRegistrationFeeShareBps(ctx)
if rewardDistributionBps > 0 && fee.Amount.GT(math.ZeroInt()) {
rewardDistributionAmount := fee.Amount.MulRaw(int64(rewardDistributionBps)).QuoRaw(10000)
if rewardDistributionAmount.IsPositive() {
rewardDistributionCoin := sdk.NewCoin(fee.Denom, rewardDistributionAmount)
err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, actiontypes.ModuleName, sntypes.ModuleName, sdk.NewCoins(rewardDistributionCoin))
if err != nil {
return errors.Wrap(err, "failed to send reward-distribution fee share")
}
fee.Amount = fee.Amount.Sub(rewardDistributionAmount)
}
}
}

params := k.GetParams(ctx)
if params.FoundationFeeShare != "" {
foundationFeeShareDec, err := math.LegacyNewDecFromStr(params.FoundationFeeShare)
Expand Down
19 changes: 6 additions & 13 deletions x/action/v1/keeper/action_cascade.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,10 @@ func (h CascadeActionHandler) Process(metadataBytes []byte, msgType common.Messa
return nil, fmt.Errorf("rq_ids_ids field is required for cascade metadata")
}
// Backward-compatible fallback for finalize payloads that do not yet
// provide explicit LEP-6 artifact counts.
if metadata.IndexArtifactCount == 0 {
metadata.IndexArtifactCount = uint32(len(metadata.RqIdsIds))
}
if metadata.SymbolArtifactCount == 0 {
metadata.SymbolArtifactCount = uint32(len(metadata.RqIdsIds))
}
// 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)
default:
return nil, fmt.Errorf("unsupported message type: %s", msgType)
}
Expand Down Expand Up @@ -326,12 +323,8 @@ func (h CascadeActionHandler) GetUpdatedMetadata(ctx sdk.Context, existingMetada
IndexArtifactCount: newMetadata.GetIndexArtifactCount(),
SymbolArtifactCount: newMetadata.GetSymbolArtifactCount(),
}
if updatedMetadata.IndexArtifactCount == 0 {
updatedMetadata.IndexArtifactCount = uint32(len(updatedMetadata.RqIdsIds))
}
if updatedMetadata.SymbolArtifactCount == 0 {
updatedMetadata.SymbolArtifactCount = uint32(len(updatedMetadata.RqIdsIds))
}
updatedMetadata.IndexArtifactCount, updatedMetadata.SymbolArtifactCount =
actiontypes.CascadeArtifactCountsWithFallback(updatedMetadata)

return gogoproto.Marshal(updatedMetadata)
}
Loading
Loading