diff --git a/app/proto_bridge.go b/app/proto_bridge.go index 38159e76..ccaa8d96 100644 --- a/app/proto_bridge.go +++ b/app/proto_bridge.go @@ -4,6 +4,7 @@ import ( govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" 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/LumeraProtocol/lumera/internal/protobridge" @@ -18,5 +19,6 @@ func init() { protobridge.RegisterEnum("lumera.action.v1.ActionType", actiontypes.ActionType_value) protobridge.RegisterEnum("lumera.action.v1.ActionState", actiontypes.ActionState_value) protobridge.RegisterEnum("lumera.action.v1.HashAlgo", actiontypes.HashAlgo_value) + protobridge.RegisterEnum("lumera.audit.v1.ReporterTrustBand", audittypes.ReporterTrustBand_value) protobridge.RegisterEnum("lumera.supernode.v1.SuperNodeState", supernodetypes.SuperNodeState_value) } diff --git a/proto/lumera/audit/v1/audit.proto b/proto/lumera/audit/v1/audit.proto index 5d3f170b..d14738cc 100644 --- a/proto/lumera/audit/v1/audit.proto +++ b/proto/lumera/audit/v1/audit.proto @@ -86,11 +86,20 @@ message NodeSuspicionState { uint64 last_updated_epoch = 3; } +enum ReporterTrustBand { + REPORTER_TRUST_BAND_UNSPECIFIED = 0; + REPORTER_TRUST_BAND_NORMAL = 1; + REPORTER_TRUST_BAND_LOW_TRUST = 2; + REPORTER_TRUST_BAND_CHALLENGER_INELIGIBLE = 3; +} + // ReporterReliabilityState is the persisted storage-truth reporter reliability snapshot. message ReporterReliabilityState { string reporter_supernode_account = 1 [(cosmos_proto.scalar) = "cosmos.AccAddressString"]; int64 reliability_score = 2; uint64 last_updated_epoch = 3; + ReporterTrustBand trust_band = 4; + uint64 contradiction_count = 5; } // TicketDeteriorationState is the persisted storage-truth ticket deterioration snapshot. @@ -101,6 +110,13 @@ message TicketDeteriorationState { uint64 active_heal_op_id = 4; uint64 probation_until_epoch = 5; uint64 last_heal_epoch = 6; + uint64 last_failure_epoch = 7; + uint32 recent_failure_epoch_count = 8; + uint64 contradiction_count = 9; + string last_target_supernode_account = 10 [(cosmos_proto.scalar) = "cosmos.AccAddressString"]; + string last_reporter_supernode_account = 11 [(cosmos_proto.scalar) = "cosmos.AccAddressString"]; + StorageProofResultClass last_result_class = 12; + uint64 last_result_epoch = 13; } enum HealOpStatus { diff --git a/x/audit/v1/keeper/abci.go b/x/audit/v1/keeper/abci.go index 3cc0df71..502dc61b 100644 --- a/x/audit/v1/keeper/abci.go +++ b/x/audit/v1/keeper/abci.go @@ -52,5 +52,9 @@ func (k Keeper) EndBlocker(ctx context.Context) error { return err } + if err := k.ProcessStorageTruthHealOpsAtEpochEnd(sdkCtx, epoch.EpochID, params); err != nil { + return err + } + return k.PruneOldEpochs(sdkCtx, epoch.EpochID, params) } diff --git a/x/audit/v1/keeper/msg_storage_truth.go b/x/audit/v1/keeper/msg_storage_truth.go new file mode 100644 index 00000000..386786d6 --- /dev/null +++ b/x/audit/v1/keeper/msg_storage_truth.go @@ -0,0 +1,289 @@ +package keeper + +import ( + "context" + "fmt" + "strconv" + "strings" + + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/LumeraProtocol/lumera/x/audit/v1/types" +) + +func (m msgServer) SubmitStorageRecheckEvidence(ctx context.Context, req *types.MsgSubmitStorageRecheckEvidence) (*types.MsgSubmitStorageRecheckEvidenceResponse, error) { + if req == nil { + return nil, errorsmod.Wrap(types.ErrInvalidSigner, "empty request") + } + if req.Creator == "" { + return nil, errorsmod.Wrap(types.ErrInvalidSigner, "creator is required") + } + if req.ChallengedSupernodeAccount == "" { + return nil, errorsmod.Wrap(types.ErrInvalidRecheckEvidence, "challenged_supernode_account is required") + } + if req.ChallengedSupernodeAccount == req.Creator { + return nil, errorsmod.Wrap(types.ErrInvalidRecheckEvidence, "challenged_supernode_account must not equal creator") + } + if req.TicketId == "" { + return nil, errorsmod.Wrap(types.ErrInvalidRecheckEvidence, "ticket_id is required") + } + if req.ChallengedResultTranscriptHash == "" { + return nil, errorsmod.Wrap(types.ErrInvalidRecheckEvidence, "challenged_result_transcript_hash is required") + } + if req.RecheckTranscriptHash == "" { + return nil, errorsmod.Wrap(types.ErrInvalidRecheckEvidence, "recheck_transcript_hash is required") + } + + sdkCtx := sdk.UnwrapSDKContext(ctx) + if _, found := m.GetEpochAnchor(sdkCtx, req.EpochId); !found { + return nil, errorsmod.Wrapf(types.ErrInvalidEpochID, "epoch anchor not found for epoch_id %d", req.EpochId) + } + + if _, found, err := m.supernodeKeeper.GetSuperNodeByAccount(sdkCtx, req.Creator); err != nil { + return nil, err + } else if !found { + return nil, errorsmod.Wrap(types.ErrReporterNotFound, "creator is not a registered supernode") + } + if _, found, err := m.supernodeKeeper.GetSuperNodeByAccount(sdkCtx, req.ChallengedSupernodeAccount); err != nil { + return nil, err + } else if !found { + return nil, errorsmod.Wrap(types.ErrInvalidRecheckEvidence, "challenged_supernode_account is not a registered supernode") + } + + switch req.RecheckResultClass { + case types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_PASS, + types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_HASH_MISMATCH, + types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_TIMEOUT_OR_NO_RESPONSE, + types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_OBSERVER_QUORUM_FAIL, + types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_INVALID_TRANSCRIPT, + types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_RECHECK_CONFIRMED_FAIL: + default: + return nil, errorsmod.Wrap(types.ErrInvalidRecheckEvidence, "recheck_result_class is invalid") + } + + return nil, errorsmod.Wrap( + types.ErrNotImplemented, + "storage recheck submission is not active in the LEP-6 heal-op lifecycle milestone", + ) +} + +func (m msgServer) ClaimHealComplete(ctx context.Context, req *types.MsgClaimHealComplete) (*types.MsgClaimHealCompleteResponse, error) { + if req == nil { + return nil, errorsmod.Wrap(types.ErrInvalidSigner, "empty request") + } + if req.Creator == "" { + return nil, errorsmod.Wrap(types.ErrInvalidSigner, "creator is required") + } + if req.HealOpId == 0 { + return nil, errorsmod.Wrap(types.ErrHealOpNotFound, "heal_op_id is required") + } + if req.TicketId == "" { + return nil, errorsmod.Wrap(types.ErrHealOpTicketMismatch, "ticket_id is required") + } + if req.HealManifestHash == "" { + return nil, errorsmod.Wrap(types.ErrHealOpInvalidState, "heal_manifest_hash is required") + } + + sdkCtx := sdk.UnwrapSDKContext(ctx) + healOp, found := m.GetHealOp(sdkCtx, req.HealOpId) + if !found { + return nil, errorsmod.Wrapf(types.ErrHealOpNotFound, "heal op %d not found", req.HealOpId) + } + if healOp.TicketId != req.TicketId { + return nil, errorsmod.Wrapf(types.ErrHealOpTicketMismatch, "ticket_id %q does not match heal op ticket_id %q", req.TicketId, healOp.TicketId) + } + if healOp.HealerSupernodeAccount != req.Creator { + return nil, errorsmod.Wrap(types.ErrHealOpUnauthorized, "creator is not assigned healer for this heal op") + } + if healOp.Status != types.HealOpStatus_HEAL_OP_STATUS_SCHEDULED && healOp.Status != types.HealOpStatus_HEAL_OP_STATUS_IN_PROGRESS { + return nil, errorsmod.Wrapf(types.ErrHealOpInvalidState, "heal op status %s does not accept healer completion claim", healOp.Status.String()) + } + + healOp.Status = types.HealOpStatus_HEAL_OP_STATUS_HEALER_REPORTED + healOp.UpdatedHeight = uint64(sdkCtx.BlockHeight()) + healOp.ResultHash = req.HealManifestHash + healOp.Notes = appendStorageTruthNote(healOp.Notes, req.Details) + + // Single-node networks may not have verifier assignments; finalize immediately. + if len(healOp.VerifierSupernodeAccounts) == 0 { + // Details were already appended above when marking healer-reported. + if err := m.finalizeHealOp(sdkCtx, healOp, true, req.HealManifestHash, ""); err != nil { + return nil, err + } + sdkCtx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeHealOpVerified, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(types.AttributeKeyHealOpID, strconv.FormatUint(healOp.HealOpId, 10)), + sdk.NewAttribute(types.AttributeKeyTicketID, healOp.TicketId), + sdk.NewAttribute(types.AttributeKeyHealerSupernodeAccount, req.Creator), + ), + ) + return &types.MsgClaimHealCompleteResponse{}, nil + } + + if err := m.SetHealOp(sdkCtx, healOp); err != nil { + return nil, err + } + + sdkCtx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeHealOpHealerReported, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(types.AttributeKeyHealOpID, strconv.FormatUint(healOp.HealOpId, 10)), + sdk.NewAttribute(types.AttributeKeyTicketID, healOp.TicketId), + sdk.NewAttribute(types.AttributeKeyHealerSupernodeAccount, req.Creator), + sdk.NewAttribute(types.AttributeKeyTranscriptHash, req.HealManifestHash), + ), + ) + + return &types.MsgClaimHealCompleteResponse{}, nil +} + +func (m msgServer) SubmitHealVerification(ctx context.Context, req *types.MsgSubmitHealVerification) (*types.MsgSubmitHealVerificationResponse, error) { + if req == nil { + return nil, errorsmod.Wrap(types.ErrInvalidSigner, "empty request") + } + if req.Creator == "" { + return nil, errorsmod.Wrap(types.ErrInvalidSigner, "creator is required") + } + if req.HealOpId == 0 { + return nil, errorsmod.Wrap(types.ErrHealOpNotFound, "heal_op_id is required") + } + if req.VerificationHash == "" { + return nil, errorsmod.Wrap(types.ErrHealOpInvalidState, "verification_hash is required") + } + + sdkCtx := sdk.UnwrapSDKContext(ctx) + healOp, found := m.GetHealOp(sdkCtx, req.HealOpId) + if !found { + return nil, errorsmod.Wrapf(types.ErrHealOpNotFound, "heal op %d not found", req.HealOpId) + } + if healOp.Status != types.HealOpStatus_HEAL_OP_STATUS_HEALER_REPORTED { + return nil, errorsmod.Wrapf(types.ErrHealOpInvalidState, "heal op status %s does not accept verification", healOp.Status.String()) + } + if !containsString(healOp.VerifierSupernodeAccounts, req.Creator) { + return nil, errorsmod.Wrap(types.ErrHealOpUnauthorized, "creator is not assigned verifier for this heal op") + } + if m.HasHealOpVerification(sdkCtx, req.HealOpId, req.Creator) { + return nil, errorsmod.Wrap(types.ErrHealVerificationExists, "verification already submitted by creator") + } + + m.SetHealOpVerification(sdkCtx, req.HealOpId, req.Creator, req.Verified) + + verifications, err := m.GetAllHealOpVerifications(sdkCtx, req.HealOpId) + if err != nil { + return nil, err + } + + positive := 0 + negative := 0 + for _, verifier := range healOp.VerifierSupernodeAccounts { + v, ok := verifications[verifier] + if !ok { + continue + } + if v { + positive++ + } else { + negative++ + } + } + + if negative > 0 { + if err := m.finalizeHealOp(sdkCtx, healOp, false, req.VerificationHash, req.Details); err != nil { + return nil, err + } + sdkCtx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeHealOpFailed, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(types.AttributeKeyHealOpID, strconv.FormatUint(healOp.HealOpId, 10)), + sdk.NewAttribute(types.AttributeKeyTicketID, healOp.TicketId), + sdk.NewAttribute(types.AttributeKeyVerifierSupernodeAccount, req.Creator), + sdk.NewAttribute(types.AttributeKeyVerified, strconv.FormatBool(req.Verified)), + ), + ) + return &types.MsgSubmitHealVerificationResponse{}, nil + } + + if positive == len(healOp.VerifierSupernodeAccounts) { + if err := m.finalizeHealOp(sdkCtx, healOp, true, req.VerificationHash, req.Details); err != nil { + return nil, err + } + sdkCtx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeHealOpVerified, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(types.AttributeKeyHealOpID, strconv.FormatUint(healOp.HealOpId, 10)), + sdk.NewAttribute(types.AttributeKeyTicketID, healOp.TicketId), + sdk.NewAttribute(types.AttributeKeyVerifierSupernodeAccount, req.Creator), + sdk.NewAttribute(types.AttributeKeyVerificationHash, req.VerificationHash), + ), + ) + return &types.MsgSubmitHealVerificationResponse{}, nil + } + + return &types.MsgSubmitHealVerificationResponse{}, nil +} + +func (m msgServer) finalizeHealOp( + ctx sdk.Context, + healOp types.HealOp, + verified bool, + verificationHash string, + details string, +) error { + if verified { + healOp.Status = types.HealOpStatus_HEAL_OP_STATUS_VERIFIED + } else { + healOp.Status = types.HealOpStatus_HEAL_OP_STATUS_FAILED + } + healOp.UpdatedHeight = uint64(ctx.BlockHeight()) + if verificationHash != "" { + healOp.ResultHash = verificationHash + } + healOp.Notes = appendStorageTruthNote(healOp.Notes, details) + if err := m.SetHealOp(ctx, healOp); err != nil { + return err + } + + ticketState, found := m.GetTicketDeteriorationState(ctx, healOp.TicketId) + if !found { + return nil + } + if ticketState.ActiveHealOpId == healOp.HealOpId { + ticketState.ActiveHealOpId = 0 + } + if verified { + params := m.GetParams(ctx) + currentEpoch, err := deriveEpochAtHeight(ctx.BlockHeight(), params) + if err != nil { + return err + } + ticketState.LastHealEpoch = currentEpoch.EpochID + ticketState.ProbationUntilEpoch = currentEpoch.EpochID + uint64(params.StorageTruthProbationEpochs) + } + return m.SetTicketDeteriorationState(ctx, ticketState) +} + +func containsString(list []string, value string) bool { + for _, v := range list { + if v == value { + return true + } + } + return false +} + +func appendStorageTruthNote(existing, note string) string { + note = strings.TrimSpace(note) + if note == "" { + return existing + } + if existing == "" { + return note + } + return fmt.Sprintf("%s | %s", existing, note) +} diff --git a/x/audit/v1/keeper/msg_storage_truth_placeholders.go b/x/audit/v1/keeper/msg_storage_truth_placeholders.go deleted file mode 100644 index 48e21846..00000000 --- a/x/audit/v1/keeper/msg_storage_truth_placeholders.go +++ /dev/null @@ -1,39 +0,0 @@ -package keeper - -import ( - "context" - - errorsmod "cosmossdk.io/errors" - - "github.com/LumeraProtocol/lumera/x/audit/v1/types" -) - -func (m msgServer) SubmitStorageRecheckEvidence(_ context.Context, req *types.MsgSubmitStorageRecheckEvidence) (*types.MsgSubmitStorageRecheckEvidenceResponse, error) { - if req == nil { - return nil, errorsmod.Wrap(types.ErrInvalidSigner, "empty request") - } - if req.Creator == "" { - return nil, errorsmod.Wrap(types.ErrInvalidSigner, "creator is required") - } - return nil, errorsmod.Wrap(types.ErrNotImplemented, "SubmitStorageRecheckEvidence is introduced in storage-truth foundation and implemented in a later PR") -} - -func (m msgServer) ClaimHealComplete(_ context.Context, req *types.MsgClaimHealComplete) (*types.MsgClaimHealCompleteResponse, error) { - if req == nil { - return nil, errorsmod.Wrap(types.ErrInvalidSigner, "empty request") - } - if req.Creator == "" { - return nil, errorsmod.Wrap(types.ErrInvalidSigner, "creator is required") - } - return nil, errorsmod.Wrap(types.ErrNotImplemented, "ClaimHealComplete is introduced in storage-truth foundation and implemented in a later PR") -} - -func (m msgServer) SubmitHealVerification(_ context.Context, req *types.MsgSubmitHealVerification) (*types.MsgSubmitHealVerificationResponse, error) { - if req == nil { - return nil, errorsmod.Wrap(types.ErrInvalidSigner, "empty request") - } - if req.Creator == "" { - return nil, errorsmod.Wrap(types.ErrInvalidSigner, "creator is required") - } - return nil, errorsmod.Wrap(types.ErrNotImplemented, "SubmitHealVerification is introduced in storage-truth foundation and implemented in a later PR") -} diff --git a/x/audit/v1/keeper/msg_storage_truth_placeholders_test.go b/x/audit/v1/keeper/msg_storage_truth_placeholders_test.go deleted file mode 100644 index 9e48fc3e..00000000 --- a/x/audit/v1/keeper/msg_storage_truth_placeholders_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package keeper_test - -import ( - "testing" - - "github.com/LumeraProtocol/lumera/x/audit/v1/keeper" - "github.com/LumeraProtocol/lumera/x/audit/v1/types" - "github.com/stretchr/testify/require" -) - -func TestMsgSubmitStorageRecheckEvidencePlaceholder(t *testing.T) { - f := initFixture(t) - ms := keeper.NewMsgServerImpl(f.keeper) - - _, err := ms.SubmitStorageRecheckEvidence(f.ctx, nil) - require.Error(t, err) - require.Contains(t, err.Error(), "empty request") - - _, err = ms.SubmitStorageRecheckEvidence(f.ctx, &types.MsgSubmitStorageRecheckEvidence{}) - require.Error(t, err) - require.Contains(t, err.Error(), "creator is required") - - _, err = ms.SubmitStorageRecheckEvidence(f.ctx, &types.MsgSubmitStorageRecheckEvidence{ - Creator: "lumera1creator111111111111111111111111r0jv6", - EpochId: 1, - ChallengedSupernodeAccount: "lumera1subject111111111111111111111111f4pnj", - TicketId: "ticket-1", - }) - require.Error(t, err) - require.Contains(t, err.Error(), types.ErrNotImplemented.Error()) -} - -func TestMsgClaimHealCompletePlaceholder(t *testing.T) { - f := initFixture(t) - ms := keeper.NewMsgServerImpl(f.keeper) - - _, err := ms.ClaimHealComplete(f.ctx, &types.MsgClaimHealComplete{ - Creator: "lumera1creator222222222222222222222222jhx4s", - HealOpId: 3, - TicketId: "ticket-3", - }) - require.Error(t, err) - require.Contains(t, err.Error(), types.ErrNotImplemented.Error()) -} - -func TestMsgSubmitHealVerificationPlaceholder(t *testing.T) { - f := initFixture(t) - ms := keeper.NewMsgServerImpl(f.keeper) - - _, err := ms.SubmitHealVerification(f.ctx, &types.MsgSubmitHealVerification{ - Creator: "lumera1creator3333333333333333333333333v56r", - HealOpId: 7, - Verified: true, - }) - require.Error(t, err) - require.Contains(t, err.Error(), types.ErrNotImplemented.Error()) -} diff --git a/x/audit/v1/keeper/msg_storage_truth_test.go b/x/audit/v1/keeper/msg_storage_truth_test.go new file mode 100644 index 00000000..76bfdb9b --- /dev/null +++ b/x/audit/v1/keeper/msg_storage_truth_test.go @@ -0,0 +1,253 @@ +package keeper_test + +import ( + "testing" + + "github.com/LumeraProtocol/lumera/x/audit/v1/keeper" + "github.com/LumeraProtocol/lumera/x/audit/v1/types" + sntypes "github.com/LumeraProtocol/lumera/x/supernode/v1/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestMsgSubmitStorageRecheckEvidence(t *testing.T) { + f := initFixture(t) + f.ctx = f.ctx.WithBlockHeight(1).WithEventManager(sdk.NewEventManager()) + ms := keeper.NewMsgServerImpl(f.keeper) + + creator := "sn-aaa-rechecker" + challenged := "sn-bbb-target" + + f.supernodeKeeper.EXPECT(). + GetSuperNodeByAccount(gomock.Any(), creator). + Return(sntypes.SuperNode{}, true, nil). + AnyTimes() + f.supernodeKeeper.EXPECT(). + GetSuperNodeByAccount(gomock.Any(), challenged). + Return(sntypes.SuperNode{}, true, nil). + AnyTimes() + + seedEpochAnchorForReportTest(t, f, 0, []string{creator, challenged}, []string{creator, challenged}) + + _, err := ms.SubmitStorageRecheckEvidence(f.ctx, nil) + require.Error(t, err) + + _, err = ms.SubmitStorageRecheckEvidence(f.ctx, &types.MsgSubmitStorageRecheckEvidence{ + Creator: creator, + EpochId: 0, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "challenged_supernode_account is required") + + _, err = ms.SubmitStorageRecheckEvidence(f.ctx, &types.MsgSubmitStorageRecheckEvidence{ + Creator: creator, + EpochId: 0, + ChallengedSupernodeAccount: creator, + TicketId: "ticket-1", + ChallengedResultTranscriptHash: "old-hash", + RecheckTranscriptHash: "new-hash", + RecheckResultClass: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_PASS, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "must not equal creator") + + _, err = ms.SubmitStorageRecheckEvidence(f.ctx, &types.MsgSubmitStorageRecheckEvidence{ + Creator: creator, + EpochId: 0, + ChallengedSupernodeAccount: challenged, + TicketId: "ticket-1", + ChallengedResultTranscriptHash: "old-hash", + RecheckTranscriptHash: "new-hash", + RecheckResultClass: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_RECHECK_CONFIRMED_FAIL, + }) + require.Error(t, err) + require.Contains(t, err.Error(), types.ErrNotImplemented.Error()) + require.Contains(t, err.Error(), "not active") + + nodeState, found := f.keeper.GetNodeSuspicionState(f.ctx, challenged) + require.False(t, found) + require.Equal(t, types.NodeSuspicionState{}, nodeState) + + reporterState, found := f.keeper.GetReporterReliabilityState(f.ctx, creator) + require.False(t, found) + require.Equal(t, types.ReporterReliabilityState{}, reporterState) + + ticketState, found := f.keeper.GetTicketDeteriorationState(f.ctx, "ticket-1") + require.False(t, found) + require.Equal(t, types.TicketDeteriorationState{}, ticketState) +} + +func TestMsgClaimHealCompleteAndSubmitVerification(t *testing.T) { + f := initFixture(t) + f.ctx = f.ctx.WithBlockHeight(1).WithEventManager(sdk.NewEventManager()) + ms := keeper.NewMsgServerImpl(f.keeper) + + healOp := types.HealOp{ + HealOpId: 11, + TicketId: "ticket-11", + ScheduledEpochId: 0, + HealerSupernodeAccount: "sn-healer", + VerifierSupernodeAccounts: []string{"sn-verifier-a", "sn-verifier-b"}, + Status: types.HealOpStatus_HEAL_OP_STATUS_SCHEDULED, + CreatedHeight: 1, + UpdatedHeight: 1, + DeadlineEpochId: 1, + } + require.NoError(t, f.keeper.SetHealOp(f.ctx, healOp)) + require.NoError(t, f.keeper.SetTicketDeteriorationState(f.ctx, types.TicketDeteriorationState{ + TicketId: healOp.TicketId, + DeteriorationScore: 110, + ActiveHealOpId: healOp.HealOpId, + })) + + _, err := ms.ClaimHealComplete(f.ctx, &types.MsgClaimHealComplete{ + Creator: "sn-not-healer", + HealOpId: healOp.HealOpId, + TicketId: healOp.TicketId, + HealManifestHash: "manifest-1", + }) + require.Error(t, err) + require.Contains(t, err.Error(), types.ErrHealOpUnauthorized.Error()) + + _, err = ms.ClaimHealComplete(f.ctx, &types.MsgClaimHealComplete{ + Creator: "sn-healer", + HealOpId: healOp.HealOpId, + TicketId: healOp.TicketId, + HealManifestHash: "manifest-1", + Details: "healer completed", + }) + require.NoError(t, err) + + claimed, found := f.keeper.GetHealOp(f.ctx, healOp.HealOpId) + require.True(t, found) + require.Equal(t, types.HealOpStatus_HEAL_OP_STATUS_HEALER_REPORTED, claimed.Status) + require.Equal(t, "manifest-1", claimed.ResultHash) + require.Contains(t, claimed.Notes, "healer completed") + + _, err = ms.SubmitHealVerification(f.ctx, &types.MsgSubmitHealVerification{ + Creator: "sn-verifier-a", + HealOpId: healOp.HealOpId, + Verified: true, + VerificationHash: "verify-1", + }) + require.NoError(t, err) + + inFlight, found := f.keeper.GetHealOp(f.ctx, healOp.HealOpId) + require.True(t, found) + require.Equal(t, types.HealOpStatus_HEAL_OP_STATUS_HEALER_REPORTED, inFlight.Status) + + _, err = ms.SubmitHealVerification(f.ctx, &types.MsgSubmitHealVerification{ + Creator: "sn-verifier-a", + HealOpId: healOp.HealOpId, + Verified: true, + VerificationHash: "verify-1-repeat", + }) + require.Error(t, err) + require.Contains(t, err.Error(), types.ErrHealVerificationExists.Error()) + + _, err = ms.SubmitHealVerification(f.ctx, &types.MsgSubmitHealVerification{ + Creator: "sn-verifier-b", + HealOpId: healOp.HealOpId, + Verified: true, + VerificationHash: "verify-2", + }) + require.NoError(t, err) + + finalized, found := f.keeper.GetHealOp(f.ctx, healOp.HealOpId) + require.True(t, found) + require.Equal(t, types.HealOpStatus_HEAL_OP_STATUS_VERIFIED, finalized.Status) + + ticketState, found := f.keeper.GetTicketDeteriorationState(f.ctx, healOp.TicketId) + require.True(t, found) + require.Equal(t, uint64(0), ticketState.ActiveHealOpId) + require.Equal(t, uint64(0), ticketState.LastHealEpoch) + require.Equal(t, uint64(types.DefaultStorageTruthProbationEpochs), ticketState.ProbationUntilEpoch) +} + +func TestMsgSubmitHealVerification_FailedPath(t *testing.T) { + f := initFixture(t) + f.ctx = f.ctx.WithBlockHeight(1).WithEventManager(sdk.NewEventManager()) + ms := keeper.NewMsgServerImpl(f.keeper) + + healOp := types.HealOp{ + HealOpId: 12, + TicketId: "ticket-12", + ScheduledEpochId: 0, + HealerSupernodeAccount: "sn-healer", + VerifierSupernodeAccounts: []string{"sn-verifier-a", "sn-verifier-b"}, + Status: types.HealOpStatus_HEAL_OP_STATUS_HEALER_REPORTED, + CreatedHeight: 1, + UpdatedHeight: 1, + DeadlineEpochId: 1, + } + require.NoError(t, f.keeper.SetHealOp(f.ctx, healOp)) + require.NoError(t, f.keeper.SetTicketDeteriorationState(f.ctx, types.TicketDeteriorationState{ + TicketId: healOp.TicketId, + DeteriorationScore: 120, + ActiveHealOpId: healOp.HealOpId, + ProbationUntilEpoch: 17, + })) + + _, err := ms.SubmitHealVerification(f.ctx, &types.MsgSubmitHealVerification{ + Creator: "sn-verifier-a", + HealOpId: healOp.HealOpId, + Verified: false, + VerificationHash: "verify-fail", + }) + require.NoError(t, err) + + finalized, found := f.keeper.GetHealOp(f.ctx, healOp.HealOpId) + require.True(t, found) + require.Equal(t, types.HealOpStatus_HEAL_OP_STATUS_FAILED, finalized.Status) + + ticketState, found := f.keeper.GetTicketDeteriorationState(f.ctx, healOp.TicketId) + require.True(t, found) + require.Equal(t, uint64(0), ticketState.ActiveHealOpId) + // Failed verification does not move probation/last-heal markers. + require.Equal(t, uint64(17), ticketState.ProbationUntilEpoch) + require.Equal(t, uint64(0), ticketState.LastHealEpoch) +} + +func TestMsgClaimHealComplete_SingleNodeFinalizesImmediately(t *testing.T) { + f := initFixture(t) + f.ctx = f.ctx.WithBlockHeight(1).WithEventManager(sdk.NewEventManager()) + ms := keeper.NewMsgServerImpl(f.keeper) + + healOp := types.HealOp{ + HealOpId: 21, + TicketId: "ticket-single-node", + ScheduledEpochId: 0, + HealerSupernodeAccount: "sn-healer", + Status: types.HealOpStatus_HEAL_OP_STATUS_SCHEDULED, + CreatedHeight: 1, + UpdatedHeight: 1, + DeadlineEpochId: 1, + } + require.NoError(t, f.keeper.SetHealOp(f.ctx, healOp)) + require.NoError(t, f.keeper.SetTicketDeteriorationState(f.ctx, types.TicketDeteriorationState{ + TicketId: healOp.TicketId, + ActiveHealOpId: healOp.HealOpId, + })) + + _, err := ms.ClaimHealComplete(f.ctx, &types.MsgClaimHealComplete{ + Creator: "sn-healer", + HealOpId: healOp.HealOpId, + TicketId: healOp.TicketId, + HealManifestHash: "manifest-single", + Details: "single node finalized", + }) + require.NoError(t, err) + + finalized, found := f.keeper.GetHealOp(f.ctx, healOp.HealOpId) + require.True(t, found) + require.Equal(t, types.HealOpStatus_HEAL_OP_STATUS_VERIFIED, finalized.Status) + require.Equal(t, "manifest-single", finalized.ResultHash) + require.Equal(t, "single node finalized", finalized.Notes) + + ticketState, found := f.keeper.GetTicketDeteriorationState(f.ctx, healOp.TicketId) + require.True(t, found) + require.Equal(t, uint64(0), ticketState.ActiveHealOpId) + require.Equal(t, uint64(0), ticketState.LastHealEpoch) + require.Equal(t, uint64(types.DefaultStorageTruthProbationEpochs), ticketState.ProbationUntilEpoch) +} diff --git a/x/audit/v1/keeper/msg_submit_epoch_report.go b/x/audit/v1/keeper/msg_submit_epoch_report.go index 497e8f79..98d6d0d3 100644 --- a/x/audit/v1/keeper/msg_submit_epoch_report.go +++ b/x/audit/v1/keeper/msg_submit_epoch_report.go @@ -160,5 +160,9 @@ func (m msgServer) SubmitEpochReport(ctx context.Context, req *types.MsgSubmitEp m.SetStorageChallengeReportIndex(sdkCtx, supernodeAccount, req.EpochId, reporterAccount) } + if err := m.applyStorageTruthScores(sdkCtx, req.EpochId, reporterAccount, req.StorageProofResults); err != nil { + return nil, err + } + return &types.MsgSubmitEpochReportResponse{}, nil } diff --git a/x/audit/v1/keeper/msg_submit_epoch_report_storage_truth_scores_test.go b/x/audit/v1/keeper/msg_submit_epoch_report_storage_truth_scores_test.go new file mode 100644 index 00000000..915b406d --- /dev/null +++ b/x/audit/v1/keeper/msg_submit_epoch_report_storage_truth_scores_test.go @@ -0,0 +1,601 @@ +package keeper_test + +import ( + "testing" + + "github.com/LumeraProtocol/lumera/x/audit/v1/keeper" + "github.com/LumeraProtocol/lumera/x/audit/v1/types" + sntypes "github.com/LumeraProtocol/lumera/x/supernode/v1/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func fullOpenPortStates() []types.PortState { + portStates := make([]types.PortState, len(types.DefaultRequiredOpenPorts)) + for i := range portStates { + portStates[i] = types.PortState_PORT_STATE_OPEN + } + return portStates +} + +func baseStorageProofResult(class types.StorageProofResultClass) *types.StorageProofResult { + result := &types.StorageProofResult{ + TargetSupernodeAccount: "sn-bbb-target", + ChallengerSupernodeAccount: "sn-aaa-reporter", + BucketType: types.StorageProofBucketType_STORAGE_PROOF_BUCKET_TYPE_RECENT, + ResultClass: class, + TranscriptHash: "tx-hash-1", + } + + if class == types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_NO_ELIGIBLE_TICKET { + result.ArtifactClass = types.StorageProofArtifactClass_STORAGE_PROOF_ARTIFACT_CLASS_UNSPECIFIED + return result + } + + result.TicketId = "ticket-1" + result.ArtifactClass = types.StorageProofArtifactClass_STORAGE_PROOF_ARTIFACT_CLASS_INDEX + result.ArtifactOrdinal = 1 + result.ArtifactKey = "artifact-key-1" + return result +} + +func TestSubmitEpochReport_StorageTruthScoresByResultClass(t *testing.T) { + tests := []struct { + name string + class types.StorageProofResultClass + bucket types.StorageProofBucketType + expectedNodeScore *int64 + expectedReporter int64 + expectedTicketScore *int64 + expectedTicketID string + }{ + { + name: "pass", + class: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_PASS, + bucket: types.StorageProofBucketType_STORAGE_PROOF_BUCKET_TYPE_RECENT, + expectedNodeScore: int64Ptr(-2), + expectedReporter: 2, + expectedTicketScore: int64Ptr(-3), + expectedTicketID: "ticket-1", + }, + { + name: "hash mismatch", + class: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_HASH_MISMATCH, + bucket: types.StorageProofBucketType_STORAGE_PROOF_BUCKET_TYPE_RECENT, + expectedNodeScore: int64Ptr(12), + expectedReporter: 1, + expectedTicketScore: int64Ptr(12), + expectedTicketID: "ticket-1", + }, + { + name: "timeout", + class: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_TIMEOUT_OR_NO_RESPONSE, + bucket: types.StorageProofBucketType_STORAGE_PROOF_BUCKET_TYPE_RECENT, + expectedNodeScore: int64Ptr(4), + expectedReporter: -1, + expectedTicketScore: int64Ptr(4), + expectedTicketID: "ticket-1", + }, + { + name: "observer quorum fail", + class: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_OBSERVER_QUORUM_FAIL, + bucket: types.StorageProofBucketType_STORAGE_PROOF_BUCKET_TYPE_RECENT, + expectedNodeScore: int64Ptr(3), + expectedReporter: -3, + expectedTicketScore: int64Ptr(5), + expectedTicketID: "ticket-1", + }, + { + name: "no eligible ticket", + class: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_NO_ELIGIBLE_TICKET, + bucket: types.StorageProofBucketType_STORAGE_PROOF_BUCKET_TYPE_RECENT, + expectedNodeScore: nil, + expectedReporter: 1, + expectedTicketScore: nil, + }, + { + name: "invalid transcript", + class: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_INVALID_TRANSCRIPT, + bucket: types.StorageProofBucketType_STORAGE_PROOF_BUCKET_TYPE_RECENT, + expectedNodeScore: nil, + expectedReporter: -8, + expectedTicketScore: nil, + }, + { + name: "recheck confirmed fail", + class: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_RECHECK_CONFIRMED_FAIL, + bucket: types.StorageProofBucketType_STORAGE_PROOF_BUCKET_TYPE_RECHECK, + expectedNodeScore: int64Ptr(20), + expectedReporter: 3, + expectedTicketScore: int64Ptr(20), + expectedTicketID: "ticket-1", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + f := initFixture(t) + f.ctx = f.ctx.WithBlockHeight(1).WithEventManager(sdk.NewEventManager()) + ms := keeper.NewMsgServerImpl(f.keeper) + + reporter := "sn-aaa-reporter" + target := "sn-bbb-target" + f.supernodeKeeper.EXPECT(). + GetSuperNodeByAccount(gomock.Any(), reporter). + Return(sntypes.SuperNode{}, true, nil). + AnyTimes() + + seedEpochAnchorForReportTest(t, f, 0, []string{reporter, target}, []string{reporter, target}) + + result := baseStorageProofResult(tc.class) + result.BucketType = tc.bucket + + _, err := ms.SubmitEpochReport(f.ctx, &types.MsgSubmitEpochReport{ + Creator: reporter, + EpochId: 0, + HostReport: types.HostReport{ + InboundPortStates: fullOpenPortStates(), + }, + StorageChallengeObservations: []*types.StorageChallengeObservation{ + { + TargetSupernodeAccount: target, + PortStates: fullOpenPortStates(), + }, + }, + StorageProofResults: []*types.StorageProofResult{result}, + }) + require.NoError(t, err) + + nodeState, found := f.keeper.GetNodeSuspicionState(f.ctx, target) + if tc.expectedNodeScore == nil { + require.False(t, found) + } else { + require.True(t, found) + require.Equal(t, *tc.expectedNodeScore, nodeState.SuspicionScore) + require.Equal(t, uint64(0), nodeState.LastUpdatedEpoch) + } + + reporterState, found := f.keeper.GetReporterReliabilityState(f.ctx, reporter) + require.True(t, found) + require.Equal(t, tc.expectedReporter, reporterState.ReliabilityScore) + require.Equal(t, uint64(0), reporterState.LastUpdatedEpoch) + require.Equal(t, types.ReporterTrustBand_REPORTER_TRUST_BAND_NORMAL, reporterState.TrustBand) + + ticketState, found := f.keeper.GetTicketDeteriorationState(f.ctx, tc.expectedTicketID) + if tc.expectedTicketScore == nil { + require.False(t, found) + } else { + require.True(t, found) + require.Equal(t, *tc.expectedTicketScore, ticketState.DeteriorationScore) + require.Equal(t, uint64(0), ticketState.LastUpdatedEpoch) + require.Equal(t, target, ticketState.LastTargetSupernodeAccount) + require.Equal(t, reporter, ticketState.LastReporterSupernodeAccount) + require.Equal(t, tc.class, ticketState.LastResultClass) + require.Equal(t, uint64(0), ticketState.LastResultEpoch) + } + }) + } +} + +func TestSubmitEpochReport_StorageTruthScoresApplyDecay(t *testing.T) { + f := initFixture(t) + f.ctx = f.ctx.WithBlockHeight(1201).WithEventManager(sdk.NewEventManager()) // epoch_id = 3 + ms := keeper.NewMsgServerImpl(f.keeper) + + params := f.keeper.GetParams(f.ctx).WithDefaults() + params.StorageTruthNodeSuspicionDecayPerEpoch = 2 + params.StorageTruthReporterReliabilityDecayPerEpoch = 3 + params.StorageTruthTicketDeteriorationDecayPerEpoch = 4 + require.NoError(t, f.keeper.SetParams(f.ctx, params)) + + reporter := "sn-aaa-reporter" + target := "sn-bbb-target" + ticketID := "ticket-1" + + require.NoError(t, f.keeper.SetNodeSuspicionState(f.ctx, types.NodeSuspicionState{ + SupernodeAccount: target, + SuspicionScore: 10, + LastUpdatedEpoch: 0, + })) + require.NoError(t, f.keeper.SetReporterReliabilityState(f.ctx, types.ReporterReliabilityState{ + ReporterSupernodeAccount: reporter, + ReliabilityScore: -9, + LastUpdatedEpoch: 0, + })) + require.NoError(t, f.keeper.SetTicketDeteriorationState(f.ctx, types.TicketDeteriorationState{ + TicketId: ticketID, + DeteriorationScore: 20, + LastUpdatedEpoch: 0, + ActiveHealOpId: 9, + ProbationUntilEpoch: 11, + LastHealEpoch: 1, + })) + + f.supernodeKeeper.EXPECT(). + GetSuperNodeByAccount(gomock.Any(), reporter). + Return(sntypes.SuperNode{}, true, nil). + AnyTimes() + + seedEpochAnchorForReportTest(t, f, 3, []string{reporter, target}, []string{reporter, target}) + + result := baseStorageProofResult(types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_HASH_MISMATCH) + result.TicketId = ticketID + + _, err := ms.SubmitEpochReport(f.ctx, &types.MsgSubmitEpochReport{ + Creator: reporter, + EpochId: 3, + HostReport: types.HostReport{ + InboundPortStates: fullOpenPortStates(), + }, + StorageChallengeObservations: []*types.StorageChallengeObservation{ + { + TargetSupernodeAccount: target, + PortStates: fullOpenPortStates(), + }, + }, + StorageProofResults: []*types.StorageProofResult{result}, + }) + require.NoError(t, err) + + nodeState, found := f.keeper.GetNodeSuspicionState(f.ctx, target) + require.True(t, found) + require.Equal(t, int64(16), nodeState.SuspicionScore) // (10 - 2*3) + 12 + require.Equal(t, uint64(3), nodeState.LastUpdatedEpoch) + + reporterState, found := f.keeper.GetReporterReliabilityState(f.ctx, reporter) + require.True(t, found) + require.Equal(t, int64(1), reporterState.ReliabilityScore) // (-9 + 3*3) + 1 => 1 + require.Equal(t, uint64(3), reporterState.LastUpdatedEpoch) + require.Equal(t, types.ReporterTrustBand_REPORTER_TRUST_BAND_NORMAL, reporterState.TrustBand) + + ticketState, found := f.keeper.GetTicketDeteriorationState(f.ctx, ticketID) + require.True(t, found) + require.Equal(t, int64(20), ticketState.DeteriorationScore) // (20 - 4*3) + 12 => 20 + require.Equal(t, uint64(3), ticketState.LastUpdatedEpoch) + // Existing lifecycle metadata remains intact in PR3. + require.Equal(t, uint64(9), ticketState.ActiveHealOpId) + require.Equal(t, uint64(11), ticketState.ProbationUntilEpoch) + require.Equal(t, uint64(1), ticketState.LastHealEpoch) +} + +func TestSubmitEpochReport_StorageTruthScoreEventsAreEmitted(t *testing.T) { + f := initFixture(t) + f.ctx = f.ctx.WithBlockHeight(1).WithEventManager(sdk.NewEventManager()) + ms := keeper.NewMsgServerImpl(f.keeper) + + reporter := "sn-aaa-reporter" + target := "sn-bbb-target" + + f.supernodeKeeper.EXPECT(). + GetSuperNodeByAccount(gomock.Any(), reporter). + Return(sntypes.SuperNode{}, true, nil). + AnyTimes() + + seedEpochAnchorForReportTest(t, f, 0, []string{reporter, target}, []string{reporter, target}) + + _, err := ms.SubmitEpochReport(f.ctx, &types.MsgSubmitEpochReport{ + Creator: reporter, + EpochId: 0, + HostReport: types.HostReport{ + InboundPortStates: fullOpenPortStates(), + }, + StorageChallengeObservations: []*types.StorageChallengeObservation{ + { + TargetSupernodeAccount: target, + PortStates: fullOpenPortStates(), + }, + }, + StorageProofResults: []*types.StorageProofResult{ + baseStorageProofResult(types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_PASS), + }, + }) + require.NoError(t, err) + + events := f.ctx.EventManager().Events() + require.NotEmpty(t, events) + + var found bool + for _, event := range events { + if event.Type != types.EventTypeStorageTruthScoreUpdated { + continue + } + found = true + + attrs := make(map[string]string, len(event.Attributes)) + for _, attr := range event.Attributes { + attrs[string(attr.Key)] = string(attr.Value) + } + + require.Equal(t, types.ModuleName, attrs[sdk.AttributeKeyModule]) + require.Equal(t, "0", attrs[types.AttributeKeyEpochID]) + require.Equal(t, reporter, attrs[types.AttributeKeyReporterSupernodeAccount]) + require.Equal(t, target, attrs[types.AttributeKeyTargetSupernodeAccount]) + require.Equal(t, "ticket-1", attrs[types.AttributeKeyTicketID]) + require.Equal(t, types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_PASS.String(), attrs[types.AttributeKeyResultClass]) + require.Equal(t, types.ReporterTrustBand_REPORTER_TRUST_BAND_NORMAL.String(), attrs[types.AttributeKeyReporterTrustBand]) + require.Equal(t, "0", attrs[types.AttributeKeyRepeatedFailureCount]) + require.Equal(t, "false", attrs[types.AttributeKeyContradictionDetected]) + require.Equal(t, "-2", attrs[types.AttributeKeyNodeSuspicionScore]) + require.Equal(t, "2", attrs[types.AttributeKeyReporterReliabilityScore]) + require.Equal(t, "-3", attrs[types.AttributeKeyTicketDeteriorationScore]) + } + require.True(t, found, "expected storage truth score update event") +} + +func TestSubmitEpochReport_NoStorageProofResults_DoesNotCreateStorageTruthStates(t *testing.T) { + f := initFixture(t) + f.ctx = f.ctx.WithBlockHeight(1).WithEventManager(sdk.NewEventManager()) + ms := keeper.NewMsgServerImpl(f.keeper) + + reporter := "sn-aaa-reporter" + target := "sn-bbb-target" + + f.supernodeKeeper.EXPECT(). + GetSuperNodeByAccount(gomock.Any(), reporter). + Return(sntypes.SuperNode{}, true, nil). + AnyTimes() + + seedEpochAnchorForReportTest(t, f, 0, []string{reporter, target}, []string{reporter, target}) + + _, err := ms.SubmitEpochReport(f.ctx, &types.MsgSubmitEpochReport{ + Creator: reporter, + EpochId: 0, + HostReport: types.HostReport{ + InboundPortStates: fullOpenPortStates(), + }, + StorageChallengeObservations: []*types.StorageChallengeObservation{ + { + TargetSupernodeAccount: target, + PortStates: fullOpenPortStates(), + }, + }, + }) + require.NoError(t, err) + + _, found := f.keeper.GetNodeSuspicionState(f.ctx, target) + require.False(t, found) + _, found = f.keeper.GetReporterReliabilityState(f.ctx, reporter) + require.False(t, found) + _, found = f.keeper.GetTicketDeteriorationState(f.ctx, "ticket-1") + require.False(t, found) +} + +func TestSubmitEpochReport_LowTrustReporterScalesNodeAndTicketDeltas(t *testing.T) { + f := initFixture(t) + f.ctx = f.ctx.WithBlockHeight(1).WithEventManager(sdk.NewEventManager()) + ms := keeper.NewMsgServerImpl(f.keeper) + + params := f.keeper.GetParams(f.ctx).WithDefaults() + params.StorageTruthReporterReliabilityLowTrustThreshold = -10 + params.StorageTruthReporterReliabilityIneligibleThreshold = -50 + require.NoError(t, f.keeper.SetParams(f.ctx, params)) + + reporter := "sn-aaa-reporter" + target := "sn-bbb-target" + f.supernodeKeeper.EXPECT(). + GetSuperNodeByAccount(gomock.Any(), reporter). + Return(sntypes.SuperNode{}, true, nil). + AnyTimes() + + require.NoError(t, f.keeper.SetReporterReliabilityState(f.ctx, types.ReporterReliabilityState{ + ReporterSupernodeAccount: reporter, + ReliabilityScore: -20, + LastUpdatedEpoch: 0, + TrustBand: types.ReporterTrustBand_REPORTER_TRUST_BAND_LOW_TRUST, + })) + seedEpochAnchorForReportTest(t, f, 0, []string{reporter, target}, []string{reporter, target}) + + _, err := ms.SubmitEpochReport(f.ctx, &types.MsgSubmitEpochReport{ + Creator: reporter, + EpochId: 0, + HostReport: types.HostReport{ + InboundPortStates: fullOpenPortStates(), + }, + StorageChallengeObservations: []*types.StorageChallengeObservation{{ + TargetSupernodeAccount: target, + PortStates: fullOpenPortStates(), + }}, + StorageProofResults: []*types.StorageProofResult{ + baseStorageProofResult(types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_HASH_MISMATCH), + }, + }) + require.NoError(t, err) + + nodeState, found := f.keeper.GetNodeSuspicionState(f.ctx, target) + require.True(t, found) + require.Equal(t, int64(6), nodeState.SuspicionScore) + + ticketState, found := f.keeper.GetTicketDeteriorationState(f.ctx, "ticket-1") + require.True(t, found) + require.Equal(t, int64(6), ticketState.DeteriorationScore) + + reporterState, found := f.keeper.GetReporterReliabilityState(f.ctx, reporter) + require.True(t, found) + require.Equal(t, int64(-19), reporterState.ReliabilityScore) + require.Equal(t, types.ReporterTrustBand_REPORTER_TRUST_BAND_LOW_TRUST, reporterState.TrustBand) +} + +func TestSubmitEpochReport_RepeatedDistinctTicketFailuresEscalate(t *testing.T) { + f := initFixture(t) + f.ctx = f.ctx.WithBlockHeight(801).WithEventManager(sdk.NewEventManager()) // epoch_id = 2 + ms := keeper.NewMsgServerImpl(f.keeper) + + params := f.keeper.GetParams(f.ctx).WithDefaults() + params.StorageTruthProbationEpochs = 3 + require.NoError(t, f.keeper.SetParams(f.ctx, params)) + + reporter := "sn-aaa-reporter" + target := "sn-bbb-target" + f.supernodeKeeper.EXPECT(). + GetSuperNodeByAccount(gomock.Any(), reporter). + Return(sntypes.SuperNode{}, true, nil). + AnyTimes() + + require.NoError(t, f.keeper.SetTicketDeteriorationState(f.ctx, types.TicketDeteriorationState{ + TicketId: "ticket-1", + DeteriorationScore: 7, + LastUpdatedEpoch: 1, + LastFailureEpoch: 1, + RecentFailureEpochCount: 1, + LastTargetSupernodeAccount: target, + LastReporterSupernodeAccount: reporter, + LastResultClass: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_HASH_MISMATCH, + LastResultEpoch: 1, + })) + seedEpochAnchorForReportTest(t, f, 2, []string{reporter, target}, []string{reporter, target}) + + _, err := ms.SubmitEpochReport(f.ctx, &types.MsgSubmitEpochReport{ + Creator: reporter, + EpochId: 2, + HostReport: types.HostReport{ + InboundPortStates: fullOpenPortStates(), + }, + StorageChallengeObservations: []*types.StorageChallengeObservation{{ + TargetSupernodeAccount: target, + PortStates: fullOpenPortStates(), + }}, + StorageProofResults: []*types.StorageProofResult{ + baseStorageProofResult(types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_HASH_MISMATCH), + }, + }) + require.NoError(t, err) + + nodeState, found := f.keeper.GetNodeSuspicionState(f.ctx, target) + require.True(t, found) + require.Equal(t, int64(14), nodeState.SuspicionScore) // 12 + escalation bonus 2 + + ticketState, found := f.keeper.GetTicketDeteriorationState(f.ctx, "ticket-1") + require.True(t, found) + require.Equal(t, int64(20), ticketState.DeteriorationScore) // 7 decays to 6, then +12 +2 + require.Equal(t, uint32(2), ticketState.RecentFailureEpochCount) + require.Equal(t, uint64(2), ticketState.LastFailureEpoch) +} + +func TestSubmitEpochReport_EpochZeroFailureWindowCarriesIntoNextEpoch(t *testing.T) { + f := initFixture(t) + f.ctx = f.ctx.WithBlockHeight(401).WithEventManager(sdk.NewEventManager()) // epoch_id = 1 + ms := keeper.NewMsgServerImpl(f.keeper) + + params := f.keeper.GetParams(f.ctx).WithDefaults() + params.StorageTruthProbationEpochs = 3 + require.NoError(t, f.keeper.SetParams(f.ctx, params)) + + reporter := "sn-aaa-reporter" + target := "sn-bbb-target" + f.supernodeKeeper.EXPECT(). + GetSuperNodeByAccount(gomock.Any(), reporter). + Return(sntypes.SuperNode{}, true, nil). + AnyTimes() + + require.NoError(t, f.keeper.SetTicketDeteriorationState(f.ctx, types.TicketDeteriorationState{ + TicketId: "ticket-epoch-zero", + DeteriorationScore: 12, + LastUpdatedEpoch: 0, + LastFailureEpoch: 0, + RecentFailureEpochCount: 1, + LastTargetSupernodeAccount: target, + LastReporterSupernodeAccount: reporter, + LastResultClass: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_HASH_MISMATCH, + LastResultEpoch: 0, + })) + seedEpochAnchorForReportTest(t, f, 1, []string{reporter, target}, []string{reporter, target}) + + result := baseStorageProofResult(types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_HASH_MISMATCH) + result.TicketId = "ticket-epoch-zero" + + _, err := ms.SubmitEpochReport(f.ctx, &types.MsgSubmitEpochReport{ + Creator: reporter, + EpochId: 1, + HostReport: types.HostReport{ + InboundPortStates: fullOpenPortStates(), + }, + StorageChallengeObservations: []*types.StorageChallengeObservation{{ + TargetSupernodeAccount: target, + PortStates: fullOpenPortStates(), + }}, + StorageProofResults: []*types.StorageProofResult{result}, + }) + require.NoError(t, err) + + nodeState, found := f.keeper.GetNodeSuspicionState(f.ctx, target) + require.True(t, found) + require.Equal(t, int64(14), nodeState.SuspicionScore) // 12 base + 2 escalation bonus from epoch-0 carryover + + ticketState, found := f.keeper.GetTicketDeteriorationState(f.ctx, "ticket-epoch-zero") + require.True(t, found) + require.Equal(t, uint32(2), ticketState.RecentFailureEpochCount) + require.Equal(t, uint64(1), ticketState.LastFailureEpoch) + require.Equal(t, int64(25), ticketState.DeteriorationScore) // 11 after decay, then +12 +2 +} + +func TestSubmitEpochReport_ContradictionsPenalizeBothReportersAndTrackState(t *testing.T) { + f := initFixture(t) + f.ctx = f.ctx.WithBlockHeight(401).WithEventManager(sdk.NewEventManager()) // epoch_id = 1 + ms := keeper.NewMsgServerImpl(f.keeper) + + reporter := "sn-aaa-reporter" + previousReporter := "sn-ccc-previous" + target := "sn-bbb-target" + f.supernodeKeeper.EXPECT(). + GetSuperNodeByAccount(gomock.Any(), reporter). + Return(sntypes.SuperNode{}, true, nil). + AnyTimes() + + require.NoError(t, f.keeper.SetReporterReliabilityState(f.ctx, types.ReporterReliabilityState{ + ReporterSupernodeAccount: previousReporter, + ReliabilityScore: 10, + LastUpdatedEpoch: 0, + TrustBand: types.ReporterTrustBand_REPORTER_TRUST_BAND_NORMAL, + })) + require.NoError(t, f.keeper.SetTicketDeteriorationState(f.ctx, types.TicketDeteriorationState{ + TicketId: "ticket-1", + DeteriorationScore: 12, + LastUpdatedEpoch: 0, + LastTargetSupernodeAccount: target, + LastReporterSupernodeAccount: previousReporter, + LastResultClass: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_HASH_MISMATCH, + LastResultEpoch: 0, + LastFailureEpoch: 0, + RecentFailureEpochCount: 1, + })) + seedEpochAnchorForReportTest(t, f, 1, []string{reporter, target}, []string{reporter, target}) + + _, err := ms.SubmitEpochReport(f.ctx, &types.MsgSubmitEpochReport{ + Creator: reporter, + EpochId: 1, + HostReport: types.HostReport{ + InboundPortStates: fullOpenPortStates(), + }, + StorageChallengeObservations: []*types.StorageChallengeObservation{{ + TargetSupernodeAccount: target, + PortStates: fullOpenPortStates(), + }}, + StorageProofResults: []*types.StorageProofResult{ + baseStorageProofResult(types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_PASS), + }, + }) + require.NoError(t, err) + + currentReporterState, found := f.keeper.GetReporterReliabilityState(f.ctx, reporter) + require.True(t, found) + require.Equal(t, int64(-4), currentReporterState.ReliabilityScore) // +2 pass delta, -6 contradiction penalty + require.Equal(t, uint64(1), currentReporterState.ContradictionCount) + require.Equal(t, types.ReporterTrustBand_REPORTER_TRUST_BAND_NORMAL, currentReporterState.TrustBand) + + previousReporterState, found := f.keeper.GetReporterReliabilityState(f.ctx, previousReporter) + require.True(t, found) + require.Equal(t, int64(3), previousReporterState.ReliabilityScore) // 10 decays to 9, then -6 + require.Equal(t, uint64(1), previousReporterState.ContradictionCount) + + ticketState, found := f.keeper.GetTicketDeteriorationState(f.ctx, "ticket-1") + require.True(t, found) + require.Equal(t, uint64(1), ticketState.ContradictionCount) + require.Equal(t, reporter, ticketState.LastReporterSupernodeAccount) + require.Equal(t, types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_PASS, ticketState.LastResultClass) + require.Equal(t, uint64(1), ticketState.LastResultEpoch) +} + +func int64Ptr(v int64) *int64 { + return &v +} diff --git a/x/audit/v1/keeper/query_storage_truth_test.go b/x/audit/v1/keeper/query_storage_truth_test.go index 0cfe2ceb..82edaac4 100644 --- a/x/audit/v1/keeper/query_storage_truth_test.go +++ b/x/audit/v1/keeper/query_storage_truth_test.go @@ -5,7 +5,10 @@ import ( "github.com/LumeraProtocol/lumera/x/audit/v1/keeper" "github.com/LumeraProtocol/lumera/x/audit/v1/types" + sntypes "github.com/LumeraProtocol/lumera/x/supernode/v1/types" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -46,6 +49,8 @@ func TestReporterReliabilityStateQuery(t *testing.T) { ReporterSupernodeAccount: "lumera1reporter111111111111111111111111lyv93", ReliabilityScore: -9, LastUpdatedEpoch: 20, + TrustBand: types.ReporterTrustBand_REPORTER_TRUST_BAND_LOW_TRUST, + ContradictionCount: 2, } require.NoError(t, f.keeper.SetReporterReliabilityState(f.ctx, state)) @@ -61,10 +66,17 @@ func TestTicketDeteriorationStateQuery(t *testing.T) { qs := keeper.NewQueryServerImpl(f.keeper) state := types.TicketDeteriorationState{ - TicketId: "ticket-query-1", - DeteriorationScore: 30, - LastUpdatedEpoch: 21, - ProbationUntilEpoch: 23, + TicketId: "ticket-query-1", + DeteriorationScore: 30, + LastUpdatedEpoch: 21, + ProbationUntilEpoch: 23, + LastFailureEpoch: 19, + RecentFailureEpochCount: 2, + ContradictionCount: 1, + LastTargetSupernodeAccount: "lumera1target1111111111111111111111111w4zx", + LastReporterSupernodeAccount: "lumera1reporter111111111111111111111111lyv93", + LastResultClass: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_HASH_MISMATCH, + LastResultEpoch: 21, } require.NoError(t, f.keeper.SetTicketDeteriorationState(f.ctx, state)) @@ -121,3 +133,74 @@ func TestHealOpQueries(t *testing.T) { require.Error(t, err) require.Equal(t, codes.InvalidArgument, status.Code(err)) } + +func TestStorageTruthQueries_ReflectScoredReportIngestion(t *testing.T) { + f := initFixture(t) + f.ctx = f.ctx.WithBlockHeight(1).WithEventManager(sdk.NewEventManager()) + qs := keeper.NewQueryServerImpl(f.keeper) + ms := keeper.NewMsgServerImpl(f.keeper) + + reporter := "sn-aaa-reporter" + target := "sn-bbb-target" + + f.supernodeKeeper.EXPECT(). + GetSuperNodeByAccount(gomock.Any(), reporter). + Return(sntypes.SuperNode{}, true, nil). + AnyTimes() + + seedEpochAnchorForReportTest(t, f, 0, []string{reporter, target}, []string{reporter, target}) + + portStates := fullOpenPortStates() + _, err := ms.SubmitEpochReport(f.ctx, &types.MsgSubmitEpochReport{ + Creator: reporter, + EpochId: 0, + HostReport: types.HostReport{ + InboundPortStates: portStates, + }, + StorageChallengeObservations: []*types.StorageChallengeObservation{ + { + TargetSupernodeAccount: target, + PortStates: portStates, + }, + }, + StorageProofResults: []*types.StorageProofResult{ + { + TargetSupernodeAccount: target, + ChallengerSupernodeAccount: reporter, + TicketId: "ticket-query-score-1", + BucketType: types.StorageProofBucketType_STORAGE_PROOF_BUCKET_TYPE_RECENT, + ArtifactClass: types.StorageProofArtifactClass_STORAGE_PROOF_ARTIFACT_CLASS_INDEX, + ArtifactOrdinal: 1, + ArtifactKey: "artifact-key-1", + ResultClass: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_HASH_MISMATCH, + TranscriptHash: "transcript-hash-1", + }, + }, + }) + require.NoError(t, err) + + nodeResp, err := qs.NodeSuspicionState(f.ctx, &types.QueryNodeSuspicionStateRequest{SupernodeAccount: target}) + require.NoError(t, err) + require.Equal(t, int64(12), nodeResp.State.SuspicionScore) + require.Equal(t, uint64(0), nodeResp.State.LastUpdatedEpoch) + + reporterResp, err := qs.ReporterReliabilityState(f.ctx, &types.QueryReporterReliabilityStateRequest{ + ReporterSupernodeAccount: reporter, + }) + require.NoError(t, err) + require.Equal(t, int64(1), reporterResp.State.ReliabilityScore) + require.Equal(t, uint64(0), reporterResp.State.LastUpdatedEpoch) + require.Equal(t, types.ReporterTrustBand_REPORTER_TRUST_BAND_NORMAL, reporterResp.State.TrustBand) + require.Equal(t, uint64(0), reporterResp.State.ContradictionCount) + + ticketResp, err := qs.TicketDeteriorationState(f.ctx, &types.QueryTicketDeteriorationStateRequest{ + TicketId: "ticket-query-score-1", + }) + require.NoError(t, err) + require.Equal(t, int64(12), ticketResp.State.DeteriorationScore) + require.Equal(t, uint64(0), ticketResp.State.LastUpdatedEpoch) + require.Equal(t, target, ticketResp.State.LastTargetSupernodeAccount) + require.Equal(t, reporter, ticketResp.State.LastReporterSupernodeAccount) + require.Equal(t, types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_HASH_MISMATCH, ticketResp.State.LastResultClass) + require.Equal(t, uint64(0), ticketResp.State.LastResultEpoch) +} diff --git a/x/audit/v1/keeper/storage_truth_heal_ops.go b/x/audit/v1/keeper/storage_truth_heal_ops.go new file mode 100644 index 00000000..77887206 --- /dev/null +++ b/x/audit/v1/keeper/storage_truth_heal_ops.go @@ -0,0 +1,248 @@ +package keeper + +import ( + "fmt" + "hash/fnv" + "sort" + "strconv" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/LumeraProtocol/lumera/x/audit/v1/types" + sntypes "github.com/LumeraProtocol/lumera/x/supernode/v1/types" +) + +func (k Keeper) ProcessStorageTruthHealOpsAtEpochEnd(ctx sdk.Context, epochID uint64, params types.Params) error { + healOps, err := k.GetAllHealOps(ctx) + if err != nil { + return err + } + + healOps, err = k.expireStorageTruthHealOpsAtEpochEnd(ctx, epochID, healOps) + if err != nil { + return err + } + return k.scheduleStorageTruthHealOpsAtEpochEnd(ctx, epochID, params, healOps) +} + +func (k Keeper) expireStorageTruthHealOpsAtEpochEnd(ctx sdk.Context, epochID uint64, healOps []types.HealOp) ([]types.HealOp, error) { + for i, healOp := range healOps { + if isHealOpFinalStatus(healOp.Status) { + continue + } + if healOp.DeadlineEpochId == 0 || healOp.DeadlineEpochId > epochID { + continue + } + + healOp.Status = types.HealOpStatus_HEAL_OP_STATUS_EXPIRED + healOp.UpdatedHeight = uint64(ctx.BlockHeight()) + if err := k.SetHealOp(ctx, healOp); err != nil { + return nil, err + } + healOps[i] = healOp + + ticketState, found := k.GetTicketDeteriorationState(ctx, healOp.TicketId) + if found && ticketState.ActiveHealOpId == healOp.HealOpId { + ticketState.ActiveHealOpId = 0 + if err := k.SetTicketDeteriorationState(ctx, ticketState); err != nil { + return nil, err + } + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeHealOpExpired, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(types.AttributeKeyEpochID, strconv.FormatUint(epochID, 10)), + sdk.NewAttribute(types.AttributeKeyHealOpID, strconv.FormatUint(healOp.HealOpId, 10)), + sdk.NewAttribute(types.AttributeKeyTicketID, healOp.TicketId), + ), + ) + } + + return healOps, nil +} + +func (k Keeper) scheduleStorageTruthHealOpsAtEpochEnd(ctx sdk.Context, epochID uint64, params types.Params, healOps []types.HealOp) error { + if params.StorageTruthMaxSelfHealOpsPerEpoch == 0 { + return nil + } + + activeAccounts, err := k.storageTruthSchedulerAccounts(ctx, epochID) + if err != nil { + return err + } + if len(activeAccounts) == 0 { + return nil + } + + nonFinalByID := make(map[uint64]types.HealOp, len(healOps)) + openByTicket := make(map[string]types.HealOp, len(healOps)) + for _, healOp := range healOps { + if isHealOpFinalStatus(healOp.Status) { + continue + } + nonFinalByID[healOp.HealOpId] = healOp + openByTicket[healOp.TicketId] = healOp + } + + ticketStates, err := k.GetAllTicketDeteriorationStates(ctx) + if err != nil { + return err + } + + type candidate struct { + ticketID string + score int64 + } + candidates := make([]candidate, 0, len(ticketStates)) + + for _, state := range ticketStates { + if state.TicketId == "" { + continue + } + if state.DeteriorationScore < params.StorageTruthTicketDeteriorationHealThreshold { + continue + } + if state.ProbationUntilEpoch > epochID { + continue + } + + if state.ActiveHealOpId != 0 { + if activeOp, found := nonFinalByID[state.ActiveHealOpId]; found { + openByTicket[state.TicketId] = activeOp + continue + } + // Clear stale pointer to a non-existing/finalized op to keep state self-consistent. + state.ActiveHealOpId = 0 + if err := k.SetTicketDeteriorationState(ctx, state); err != nil { + return err + } + } + + if _, hasOpen := openByTicket[state.TicketId]; hasOpen { + continue + } + + candidates = append(candidates, candidate{ticketID: state.TicketId, score: state.DeteriorationScore}) + } + + sort.Slice(candidates, func(i, j int) bool { + if candidates[i].score == candidates[j].score { + return candidates[i].ticketID < candidates[j].ticketID + } + return candidates[i].score > candidates[j].score + }) + + scheduled := uint32(0) + for _, cand := range candidates { + if scheduled >= params.StorageTruthMaxSelfHealOpsPerEpoch { + break + } + + healer, verifiers := assignStorageTruthHealParticipants(activeAccounts, cand.ticketID, epochID) + healOpID := k.GetNextHealOpID(ctx) + healOp := types.HealOp{ + HealOpId: healOpID, + TicketId: cand.ticketID, + ScheduledEpochId: epochID, + HealerSupernodeAccount: healer, + VerifierSupernodeAccounts: verifiers, + Status: types.HealOpStatus_HEAL_OP_STATUS_SCHEDULED, + CreatedHeight: uint64(ctx.BlockHeight()), + UpdatedHeight: uint64(ctx.BlockHeight()), + DeadlineEpochId: epochID + 1, + } + + if err := k.SetHealOp(ctx, healOp); err != nil { + return err + } + k.SetNextHealOpID(ctx, healOpID+1) + + ticketState, found := k.GetTicketDeteriorationState(ctx, cand.ticketID) + if !found { + return fmt.Errorf("ticket deterioration state not found for ticket %q while scheduling heal op", cand.ticketID) + } + ticketState.ActiveHealOpId = healOpID + if err := k.SetTicketDeteriorationState(ctx, ticketState); err != nil { + return err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeHealOpScheduled, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(types.AttributeKeyEpochID, strconv.FormatUint(epochID, 10)), + sdk.NewAttribute(types.AttributeKeyHealOpID, strconv.FormatUint(healOpID, 10)), + sdk.NewAttribute(types.AttributeKeyTicketID, cand.ticketID), + sdk.NewAttribute(types.AttributeKeyHealerSupernodeAccount, healer), + sdk.NewAttribute(types.AttributeKeyDeadlineEpochID, strconv.FormatUint(healOp.DeadlineEpochId, 10)), + ), + ) + scheduled++ + } + + return nil +} + +func (k Keeper) storageTruthSchedulerAccounts(ctx sdk.Context, epochID uint64) ([]string, error) { + if anchor, found := k.GetEpochAnchor(ctx, epochID); found && len(anchor.ActiveSupernodeAccounts) > 0 { + return append([]string(nil), anchor.ActiveSupernodeAccounts...), nil + } + + active, err := k.supernodeKeeper.GetAllSuperNodes(ctx, sntypes.SuperNodeStateActive) + if err != nil { + return nil, err + } + accounts, err := supernodeAccountsFromSet(active) + if err != nil { + return nil, err + } + sort.Strings(accounts) + return accounts, nil +} + +func assignStorageTruthHealParticipants(activeAccounts []string, ticketID string, epochID uint64) (string, []string) { + if len(activeAccounts) == 0 { + return "", nil + } + + idx := deterministicStorageTruthIndex(ticketID, epochID, len(activeAccounts)) + healer := activeAccounts[idx] + + if len(activeAccounts) == 1 { + return healer, nil + } + + verifierCount := 2 + if verifierCount > len(activeAccounts)-1 { + verifierCount = len(activeAccounts) - 1 + } + verifiers := make([]string, 0, verifierCount) + for i := 1; i <= verifierCount; i++ { + verifiers = append(verifiers, activeAccounts[(idx+i)%len(activeAccounts)]) + } + return healer, verifiers +} + +func deterministicStorageTruthIndex(ticketID string, epochID uint64, n int) int { + if n <= 1 { + return 0 + } + h := fnv.New64a() + _, _ = h.Write([]byte(ticketID)) + _, _ = h.Write([]byte{0}) + _, _ = h.Write([]byte(strconv.FormatUint(epochID, 10))) + return int(h.Sum64() % uint64(n)) +} + +func isHealOpFinalStatus(status types.HealOpStatus) bool { + switch status { + case types.HealOpStatus_HEAL_OP_STATUS_VERIFIED, + types.HealOpStatus_HEAL_OP_STATUS_FAILED, + types.HealOpStatus_HEAL_OP_STATUS_EXPIRED: + return true + default: + return false + } +} diff --git a/x/audit/v1/keeper/storage_truth_heal_ops_test.go b/x/audit/v1/keeper/storage_truth_heal_ops_test.go new file mode 100644 index 00000000..610723db --- /dev/null +++ b/x/audit/v1/keeper/storage_truth_heal_ops_test.go @@ -0,0 +1,162 @@ +package keeper_test + +import ( + "testing" + + "github.com/LumeraProtocol/lumera/x/audit/v1/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestProcessStorageTruthHealOpsAtEpochEnd_SchedulesByPriority(t *testing.T) { + f := initFixture(t) + f.ctx = f.ctx.WithBlockHeight(400).WithEventManager(sdk.NewEventManager()) + + params := f.keeper.GetParams(f.ctx).WithDefaults() + params.StorageTruthTicketDeteriorationHealThreshold = 40 + params.StorageTruthMaxSelfHealOpsPerEpoch = 2 + require.NoError(t, f.keeper.SetParams(f.ctx, params)) + + activeAccounts := []string{"sn-aaa", "sn-bbb", "sn-ccc"} + seedEpochAnchorForReportTest(t, f, 0, activeAccounts, activeAccounts) + + // Existing non-final op keeps this ticket ineligible. + require.NoError(t, f.keeper.SetHealOp(f.ctx, types.HealOp{ + HealOpId: 500, + TicketId: "ticket-open", + ScheduledEpochId: 0, + HealerSupernodeAccount: "sn-aaa", + VerifierSupernodeAccounts: []string{"sn-bbb"}, + Status: types.HealOpStatus_HEAL_OP_STATUS_IN_PROGRESS, + DeadlineEpochId: 10, + })) + + require.NoError(t, f.keeper.SetTicketDeteriorationState(f.ctx, types.TicketDeteriorationState{ + TicketId: "ticket-high", + DeteriorationScore: 90, + })) + require.NoError(t, f.keeper.SetTicketDeteriorationState(f.ctx, types.TicketDeteriorationState{ + TicketId: "ticket-mid", + DeteriorationScore: 50, + })) + require.NoError(t, f.keeper.SetTicketDeteriorationState(f.ctx, types.TicketDeteriorationState{ + TicketId: "ticket-low", + DeteriorationScore: 10, // below threshold + })) + require.NoError(t, f.keeper.SetTicketDeteriorationState(f.ctx, types.TicketDeteriorationState{ + TicketId: "ticket-probation", + DeteriorationScore: 100, + ProbationUntilEpoch: 2, // epoch 0 should skip + })) + require.NoError(t, f.keeper.SetTicketDeteriorationState(f.ctx, types.TicketDeteriorationState{ + TicketId: "ticket-open", + DeteriorationScore: 200, + ActiveHealOpId: 500, + })) + + f.keeper.SetNextHealOpID(f.ctx, 100) + require.NoError(t, f.keeper.ProcessStorageTruthHealOpsAtEpochEnd(f.ctx, 0, params)) + + first, found := f.keeper.GetHealOp(f.ctx, 100) + require.True(t, found) + require.Equal(t, "ticket-high", first.TicketId) + require.Equal(t, types.HealOpStatus_HEAL_OP_STATUS_SCHEDULED, first.Status) + require.NotEmpty(t, first.HealerSupernodeAccount) + + second, found := f.keeper.GetHealOp(f.ctx, 101) + require.True(t, found) + require.Equal(t, "ticket-mid", second.TicketId) + require.Equal(t, types.HealOpStatus_HEAL_OP_STATUS_SCHEDULED, second.Status) + + ticketHigh, found := f.keeper.GetTicketDeteriorationState(f.ctx, "ticket-high") + require.True(t, found) + require.Equal(t, uint64(100), ticketHigh.ActiveHealOpId) + + ticketMid, found := f.keeper.GetTicketDeteriorationState(f.ctx, "ticket-mid") + require.True(t, found) + require.Equal(t, uint64(101), ticketMid.ActiveHealOpId) + + ticketOpen, found := f.keeper.GetTicketDeteriorationState(f.ctx, "ticket-open") + require.True(t, found) + require.Equal(t, uint64(500), ticketOpen.ActiveHealOpId) + + require.Equal(t, uint64(102), f.keeper.GetNextHealOpID(f.ctx)) +} + +func TestProcessStorageTruthHealOpsAtEpochEnd_ExpiresPastDeadline(t *testing.T) { + f := initFixture(t) + f.ctx = f.ctx.WithBlockHeight(1600).WithEventManager(sdk.NewEventManager()) // epoch 3 end + + params := f.keeper.GetParams(f.ctx).WithDefaults() + params.StorageTruthMaxSelfHealOpsPerEpoch = 0 // focus on expiry only + require.NoError(t, f.keeper.SetParams(f.ctx, params)) + + require.NoError(t, f.keeper.SetHealOp(f.ctx, types.HealOp{ + HealOpId: 700, + TicketId: "ticket-expire", + ScheduledEpochId: 1, + HealerSupernodeAccount: "sn-healer", + VerifierSupernodeAccounts: []string{"sn-verifier"}, + Status: types.HealOpStatus_HEAL_OP_STATUS_HEALER_REPORTED, + DeadlineEpochId: 3, + })) + require.NoError(t, f.keeper.SetTicketDeteriorationState(f.ctx, types.TicketDeteriorationState{ + TicketId: "ticket-expire", + DeteriorationScore: 100, + ActiveHealOpId: 700, + })) + + require.NoError(t, f.keeper.ProcessStorageTruthHealOpsAtEpochEnd(f.ctx, 3, params)) + + expired, found := f.keeper.GetHealOp(f.ctx, 700) + require.True(t, found) + require.Equal(t, types.HealOpStatus_HEAL_OP_STATUS_EXPIRED, expired.Status) + + ticketState, found := f.keeper.GetTicketDeteriorationState(f.ctx, "ticket-expire") + require.True(t, found) + require.Equal(t, uint64(0), ticketState.ActiveHealOpId) +} + +func TestProcessStorageTruthHealOpsAtEpochEnd_ExpiredOpDoesNotBlockReschedule(t *testing.T) { + f := initFixture(t) + f.ctx = f.ctx.WithBlockHeight(1600).WithEventManager(sdk.NewEventManager()) // epoch 3 end + + params := f.keeper.GetParams(f.ctx).WithDefaults() + params.StorageTruthTicketDeteriorationHealThreshold = 40 + params.StorageTruthMaxSelfHealOpsPerEpoch = 1 + require.NoError(t, f.keeper.SetParams(f.ctx, params)) + + activeAccounts := []string{"sn-aaa", "sn-bbb", "sn-ccc"} + seedEpochAnchorForReportTest(t, f, 3, activeAccounts, activeAccounts) + + require.NoError(t, f.keeper.SetHealOp(f.ctx, types.HealOp{ + HealOpId: 700, + TicketId: "ticket-expire", + ScheduledEpochId: 1, + HealerSupernodeAccount: "sn-healer", + VerifierSupernodeAccounts: []string{"sn-verifier"}, + Status: types.HealOpStatus_HEAL_OP_STATUS_HEALER_REPORTED, + DeadlineEpochId: 3, + })) + require.NoError(t, f.keeper.SetTicketDeteriorationState(f.ctx, types.TicketDeteriorationState{ + TicketId: "ticket-expire", + DeteriorationScore: 100, + ActiveHealOpId: 700, + })) + f.keeper.SetNextHealOpID(f.ctx, 800) + + require.NoError(t, f.keeper.ProcessStorageTruthHealOpsAtEpochEnd(f.ctx, 3, params)) + + expired, found := f.keeper.GetHealOp(f.ctx, 700) + require.True(t, found) + require.Equal(t, types.HealOpStatus_HEAL_OP_STATUS_EXPIRED, expired.Status) + + rescheduled, found := f.keeper.GetHealOp(f.ctx, 800) + require.True(t, found) + require.Equal(t, "ticket-expire", rescheduled.TicketId) + require.Equal(t, types.HealOpStatus_HEAL_OP_STATUS_SCHEDULED, rescheduled.Status) + + ticketState, found := f.keeper.GetTicketDeteriorationState(f.ctx, "ticket-expire") + require.True(t, found) + require.Equal(t, uint64(800), ticketState.ActiveHealOpId) +} diff --git a/x/audit/v1/keeper/storage_truth_scoring.go b/x/audit/v1/keeper/storage_truth_scoring.go new file mode 100644 index 00000000..a82d1fb2 --- /dev/null +++ b/x/audit/v1/keeper/storage_truth_scoring.go @@ -0,0 +1,513 @@ +package keeper + +import ( + "math" + "strconv" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/LumeraProtocol/lumera/x/audit/v1/types" +) + +type storageTruthScoreDeltas struct { + nodeSuspicion int64 + reporterReliability int64 + ticketDeterioration int64 +} + +type storageTruthResultBookkeeping struct { + reporterTrustBand types.ReporterTrustBand + repeatedFailureCount uint32 + contradictionDetected bool + contradictedReporter string + currentReporterPenalty int64 + contradictedReporterDelta int64 + nodeBonus int64 + ticketBonus int64 +} + +// applyStorageTruthScores updates storage-truth scoring states from report results. +// This remains shadow-safe: it only updates LEP-6 score state and emits score events. +func (k Keeper) applyStorageTruthScores( + ctx sdk.Context, + epochID uint64, + reporterAccount string, + results []*types.StorageProofResult, +) error { + if len(results) == 0 { + return nil + } + + params := k.GetParams(ctx).WithDefaults() + switch params.StorageTruthEnforcementMode { + case types.StorageTruthEnforcementMode_STORAGE_TRUTH_ENFORCEMENT_MODE_SHADOW, + types.StorageTruthEnforcementMode_STORAGE_TRUTH_ENFORCEMENT_MODE_SOFT, + types.StorageTruthEnforcementMode_STORAGE_TRUTH_ENFORCEMENT_MODE_FULL: + default: + return nil + } + + for _, result := range results { + if result == nil { + continue + } + + deltas := storageTruthScoreDeltasForResultClass(result.ResultClass) + bookkeeping, err := k.storageTruthBookkeepingForResult(ctx, epochID, reporterAccount, result, params) + if err != nil { + return err + } + + deltas.reporterReliability = addInt64Saturated(deltas.reporterReliability, bookkeeping.currentReporterPenalty) + deltas.nodeSuspicion = addInt64Saturated(deltas.nodeSuspicion, bookkeeping.nodeBonus) + deltas.ticketDeterioration = addInt64Saturated(deltas.ticketDeterioration, bookkeeping.ticketBonus) + deltas.nodeSuspicion = scaleInt64TowardZero(deltas.nodeSuspicion, reporterTrustMultiplierNumerator(bookkeeping.reporterTrustBand), 100) + deltas.ticketDeterioration = scaleInt64TowardZero(deltas.ticketDeterioration, reporterTrustMultiplierNumerator(bookkeeping.reporterTrustBand), 100) + + nodeScore, nodeUpdated, err := k.applyNodeSuspicionDelta( + ctx, + epochID, + result.TargetSupernodeAccount, + deltas.nodeSuspicion, + params.StorageTruthNodeSuspicionDecayPerEpoch, + ) + if err != nil { + return err + } + + reporterState, reporterUpdated, err := k.applyReporterReliabilityDelta( + ctx, + epochID, + reporterAccount, + deltas.reporterReliability, + params.StorageTruthReporterReliabilityDecayPerEpoch, + boolToUint64(bookkeeping.contradictionDetected), + ) + if err != nil { + return err + } + + if bookkeeping.contradictedReporter != "" && bookkeeping.contradictedReporterDelta != 0 { + if _, _, err := k.applyReporterReliabilityDelta( + ctx, + epochID, + bookkeeping.contradictedReporter, + bookkeeping.contradictedReporterDelta, + params.StorageTruthReporterReliabilityDecayPerEpoch, + 1, + ); err != nil { + return err + } + } + + ticketState, ticketUpdated, err := k.applyTicketDeteriorationDelta( + ctx, + epochID, + reporterAccount, + result, + result.TicketId, + deltas.ticketDeterioration, + params.StorageTruthTicketDeteriorationDecayPerEpoch, + ) + if err != nil { + return err + } + + if !nodeUpdated && !reporterUpdated && !ticketUpdated { + continue + } + + attrs := []sdk.Attribute{ + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(types.AttributeKeyEpochID, strconv.FormatUint(epochID, 10)), + sdk.NewAttribute(types.AttributeKeyReporterSupernodeAccount, reporterAccount), + sdk.NewAttribute(types.AttributeKeyTargetSupernodeAccount, result.TargetSupernodeAccount), + sdk.NewAttribute(types.AttributeKeyTicketID, result.TicketId), + sdk.NewAttribute(types.AttributeKeyResultClass, result.ResultClass.String()), + sdk.NewAttribute(types.AttributeKeyBucketType, result.BucketType.String()), + sdk.NewAttribute(types.AttributeKeyReporterTrustBand, reporterState.TrustBand.String()), + sdk.NewAttribute(types.AttributeKeyRepeatedFailureCount, strconv.FormatUint(uint64(ticketState.RecentFailureEpochCount), 10)), + sdk.NewAttribute(types.AttributeKeyContradictionDetected, strconv.FormatBool(bookkeeping.contradictionDetected)), + } + if nodeUpdated { + attrs = append(attrs, sdk.NewAttribute(types.AttributeKeyNodeSuspicionScore, strconv.FormatInt(nodeScore, 10))) + } + if reporterUpdated { + attrs = append(attrs, sdk.NewAttribute(types.AttributeKeyReporterReliabilityScore, strconv.FormatInt(reporterState.ReliabilityScore, 10))) + } + if ticketUpdated { + attrs = append(attrs, sdk.NewAttribute(types.AttributeKeyTicketDeteriorationScore, strconv.FormatInt(ticketState.DeteriorationScore, 10))) + } + if bookkeeping.contradictedReporter != "" { + attrs = append(attrs, sdk.NewAttribute(types.AttributeKeyContradictedReporter, bookkeeping.contradictedReporter)) + } + ctx.EventManager().EmitEvent(sdk.NewEvent(types.EventTypeStorageTruthScoreUpdated, attrs...)) + } + + return nil +} + +func (k Keeper) applyNodeSuspicionDelta( + ctx sdk.Context, + epochID uint64, + supernodeAccount string, + delta int64, + decayPerEpoch int64, +) (int64, bool, error) { + if supernodeAccount == "" { + return 0, false, nil + } + state, found := k.GetNodeSuspicionState(ctx, supernodeAccount) + if !found && delta == 0 { + return 0, false, nil + } + + current := int64(0) + if found { + current = decayTowardZero(state.SuspicionScore, decayPerEpoch, epochDelta(epochID, state.LastUpdatedEpoch)) + } + next := addInt64Saturated(current, delta) + + nextState := types.NodeSuspicionState{ + SupernodeAccount: supernodeAccount, + SuspicionScore: next, + LastUpdatedEpoch: epochID, + } + if err := k.SetNodeSuspicionState(ctx, nextState); err != nil { + return 0, false, err + } + return next, true, nil +} + +func (k Keeper) applyReporterReliabilityDelta( + ctx sdk.Context, + epochID uint64, + reporterAccount string, + delta int64, + decayPerEpoch int64, + contradictionIncrements uint64, +) (types.ReporterReliabilityState, bool, error) { + if reporterAccount == "" { + return types.ReporterReliabilityState{}, false, nil + } + state, found := k.GetReporterReliabilityState(ctx, reporterAccount) + if !found && delta == 0 && contradictionIncrements == 0 { + return types.ReporterReliabilityState{}, false, nil + } + + current := int64(0) + if found { + current = decayTowardZero(state.ReliabilityScore, decayPerEpoch, epochDelta(epochID, state.LastUpdatedEpoch)) + } + next := addInt64Saturated(current, delta) + + nextState := types.ReporterReliabilityState{ + ReporterSupernodeAccount: reporterAccount, + ReliabilityScore: next, + LastUpdatedEpoch: epochID, + TrustBand: reporterTrustBandForScore(next, k.GetParams(ctx).WithDefaults()), + ContradictionCount: state.ContradictionCount + contradictionIncrements, + } + if err := k.SetReporterReliabilityState(ctx, nextState); err != nil { + return types.ReporterReliabilityState{}, false, err + } + return nextState, true, nil +} + +func (k Keeper) applyTicketDeteriorationDelta( + ctx sdk.Context, + epochID uint64, + reporterAccount string, + result *types.StorageProofResult, + ticketID string, + delta int64, + decayPerEpoch int64, +) (types.TicketDeteriorationState, bool, error) { + if ticketID == "" { + return types.TicketDeteriorationState{}, false, nil + } + state, found := k.GetTicketDeteriorationState(ctx, ticketID) + if !found && delta == 0 { + return types.TicketDeteriorationState{}, false, nil + } + + current := int64(0) + if found { + current = decayTowardZero(state.DeteriorationScore, decayPerEpoch, epochDelta(epochID, state.LastUpdatedEpoch)) + } + next := addInt64Saturated(current, delta) + + nextState := state + nextState.TicketId = ticketID + nextState.DeteriorationScore = next + nextState.LastUpdatedEpoch = epochID + if result != nil { + if isStorageTruthFailureClass(result.ResultClass) && epochID != state.LastFailureEpoch { + nextState.LastFailureEpoch = epochID + nextState.RecentFailureEpochCount = updateRecentFailureEpochCount(state, epochID, k.GetParams(ctx).WithDefaults()) + } else if !found { + nextState.RecentFailureEpochCount = 0 + } + if result.TicketId != "" { + nextState.LastTargetSupernodeAccount = result.TargetSupernodeAccount + nextState.LastReporterSupernodeAccount = reporterAccount + nextState.LastResultClass = result.ResultClass + nextState.LastResultEpoch = epochID + if state.LastResultEpoch < epochID && + state.LastTargetSupernodeAccount == result.TargetSupernodeAccount && + storageTruthResultsContradict(state.LastResultClass, result.ResultClass) { + nextState.ContradictionCount = state.ContradictionCount + 1 + } + } + } + if err := k.SetTicketDeteriorationState(ctx, nextState); err != nil { + return types.TicketDeteriorationState{}, false, err + } + return nextState, true, nil +} + +func storageTruthScoreDeltasForResultClass(class types.StorageProofResultClass) storageTruthScoreDeltas { + switch class { + case types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_PASS: + return storageTruthScoreDeltas{ + nodeSuspicion: -2, + reporterReliability: 2, + ticketDeterioration: -3, + } + case types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_HASH_MISMATCH: + return storageTruthScoreDeltas{ + nodeSuspicion: 12, + reporterReliability: 1, + ticketDeterioration: 12, + } + case types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_TIMEOUT_OR_NO_RESPONSE: + return storageTruthScoreDeltas{ + nodeSuspicion: 4, + reporterReliability: -1, + ticketDeterioration: 4, + } + case types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_OBSERVER_QUORUM_FAIL: + return storageTruthScoreDeltas{ + nodeSuspicion: 3, + reporterReliability: -3, + ticketDeterioration: 5, + } + case types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_NO_ELIGIBLE_TICKET: + return storageTruthScoreDeltas{ + nodeSuspicion: 0, + reporterReliability: 1, + ticketDeterioration: 0, + } + case types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_INVALID_TRANSCRIPT: + return storageTruthScoreDeltas{ + nodeSuspicion: 0, + reporterReliability: -8, + ticketDeterioration: 0, + } + case types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_RECHECK_CONFIRMED_FAIL: + return storageTruthScoreDeltas{ + nodeSuspicion: 20, + reporterReliability: 3, + ticketDeterioration: 20, + } + default: + return storageTruthScoreDeltas{} + } +} + +func epochDelta(currentEpoch, lastUpdatedEpoch uint64) uint64 { + if currentEpoch <= lastUpdatedEpoch { + return 0 + } + return currentEpoch - lastUpdatedEpoch +} + +func (k Keeper) storageTruthBookkeepingForResult( + ctx sdk.Context, + epochID uint64, + reporterAccount string, + result *types.StorageProofResult, + params types.Params, +) (storageTruthResultBookkeeping, error) { + bookkeeping := storageTruthResultBookkeeping{ + reporterTrustBand: types.ReporterTrustBand_REPORTER_TRUST_BAND_NORMAL, + } + if result == nil { + return bookkeeping, nil + } + + reliabilityScore := int64(0) + if state, found := k.GetReporterReliabilityState(ctx, reporterAccount); found { + reliabilityScore = decayTowardZero(state.ReliabilityScore, params.StorageTruthReporterReliabilityDecayPerEpoch, epochDelta(epochID, state.LastUpdatedEpoch)) + } + bookkeeping.reporterTrustBand = reporterTrustBandForScore(reliabilityScore, params) + + if result.TicketId == "" { + return bookkeeping, nil + } + + ticketState, found := k.GetTicketDeteriorationState(ctx, result.TicketId) + if !found { + if isStorageTruthFailureClass(result.ResultClass) { + bookkeeping.repeatedFailureCount = 1 + } + return bookkeeping, nil + } + + if isStorageTruthFailureClass(result.ResultClass) { + bookkeeping.repeatedFailureCount = updateRecentFailureEpochCount(ticketState, epochID, params) + if bookkeeping.repeatedFailureCount > 1 && epochID != ticketState.LastFailureEpoch { + bonus := repeatedFailureEscalationBonus(bookkeeping.repeatedFailureCount) + bookkeeping.nodeBonus = bonus + bookkeeping.ticketBonus = bonus + } + } else { + bookkeeping.repeatedFailureCount = ticketState.RecentFailureEpochCount + } + + if ticketState.LastResultEpoch < epochID && + ticketState.LastTargetSupernodeAccount == result.TargetSupernodeAccount && + storageTruthResultsContradict(ticketState.LastResultClass, result.ResultClass) { + bookkeeping.contradictionDetected = true + bookkeeping.currentReporterPenalty = -6 + if ticketState.LastReporterSupernodeAccount != "" && ticketState.LastReporterSupernodeAccount != reporterAccount { + bookkeeping.contradictedReporter = ticketState.LastReporterSupernodeAccount + bookkeeping.contradictedReporterDelta = -6 + } + } + + return bookkeeping, nil +} + +func reporterTrustBandForScore(score int64, params types.Params) types.ReporterTrustBand { + switch { + case score <= params.StorageTruthReporterReliabilityIneligibleThreshold: + return types.ReporterTrustBand_REPORTER_TRUST_BAND_CHALLENGER_INELIGIBLE + case score <= params.StorageTruthReporterReliabilityLowTrustThreshold: + return types.ReporterTrustBand_REPORTER_TRUST_BAND_LOW_TRUST + default: + return types.ReporterTrustBand_REPORTER_TRUST_BAND_NORMAL + } +} + +func reporterTrustMultiplierNumerator(band types.ReporterTrustBand) int64 { + switch band { + case types.ReporterTrustBand_REPORTER_TRUST_BAND_LOW_TRUST: + return 50 + case types.ReporterTrustBand_REPORTER_TRUST_BAND_CHALLENGER_INELIGIBLE: + return 25 + default: + return 100 + } +} + +func scaleInt64TowardZero(value, numerator, denominator int64) int64 { + if denominator <= 0 || numerator <= 0 || value == 0 { + return 0 + } + if numerator >= denominator { + return value + } + if value > 0 { + return (value * numerator) / denominator + } + return -(((-value) * numerator) / denominator) +} + +func isStorageTruthFailureClass(class types.StorageProofResultClass) bool { + switch class { + case types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_HASH_MISMATCH, + types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_TIMEOUT_OR_NO_RESPONSE, + types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_OBSERVER_QUORUM_FAIL, + types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_INVALID_TRANSCRIPT, + types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_RECHECK_CONFIRMED_FAIL: + return true + default: + return false + } +} + +func storageTruthResultsContradict(prev, current types.StorageProofResultClass) bool { + prevFailure := isStorageTruthFailureClass(prev) + currentFailure := isStorageTruthFailureClass(current) + return (prev == types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_PASS && currentFailure) || + (current == types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_PASS && prevFailure) +} + +func updateRecentFailureEpochCount(state types.TicketDeteriorationState, epochID uint64, params types.Params) uint32 { + if epochID == state.LastFailureEpoch { + if state.RecentFailureEpochCount == 0 { + return 1 + } + return state.RecentFailureEpochCount + } + if state.RecentFailureEpochCount == 0 { + return 1 + } + window := uint64(params.StorageTruthProbationEpochs) + if window < 2 { + window = 2 + } + if epochDelta(epochID, state.LastFailureEpoch) > window { + return 1 + } + if state.RecentFailureEpochCount == math.MaxUint32 { + return math.MaxUint32 + } + return state.RecentFailureEpochCount + 1 +} + +func repeatedFailureEscalationBonus(count uint32) int64 { + if count <= 1 { + return 0 + } + bonusSteps := count - 1 + if bonusSteps > 3 { + bonusSteps = 3 + } + return int64(bonusSteps) * 2 +} + +func boolToUint64(v bool) uint64 { + if v { + return 1 + } + return 0 +} + +func decayTowardZero(score, decayPerEpoch int64, elapsedEpochs uint64) int64 { + if score == 0 || decayPerEpoch <= 0 || elapsedEpochs == 0 { + return score + } + decayTotal := mulInt64ByUint64Saturated(decayPerEpoch, elapsedEpochs) + if score > 0 { + if decayTotal >= score { + return 0 + } + return score - decayTotal + } + if decayTotal >= -score { + return 0 + } + return score + decayTotal +} + +func mulInt64ByUint64Saturated(v int64, m uint64) int64 { + if v <= 0 || m == 0 { + return 0 + } + if m > uint64(math.MaxInt64/v) { + return math.MaxInt64 + } + return v * int64(m) +} + +func addInt64Saturated(a, b int64) int64 { + if b > 0 && a > math.MaxInt64-b { + return math.MaxInt64 + } + if b < 0 && a < math.MinInt64-b { + return math.MinInt64 + } + return a + b +} diff --git a/x/audit/v1/keeper/storage_truth_scoring_internal_test.go b/x/audit/v1/keeper/storage_truth_scoring_internal_test.go new file mode 100644 index 00000000..24ab07cf --- /dev/null +++ b/x/audit/v1/keeper/storage_truth_scoring_internal_test.go @@ -0,0 +1,154 @@ +package keeper + +import ( + "math" + "testing" + + "github.com/LumeraProtocol/lumera/x/audit/v1/types" + "github.com/stretchr/testify/require" +) + +func TestStorageTruthScoreDeltasForResultClass(t *testing.T) { + tests := []struct { + name string + class types.StorageProofResultClass + expect storageTruthScoreDeltas + }{ + { + name: "pass", + class: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_PASS, + expect: storageTruthScoreDeltas{ + nodeSuspicion: -2, + reporterReliability: 2, + ticketDeterioration: -3, + }, + }, + { + name: "hash mismatch", + class: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_HASH_MISMATCH, + expect: storageTruthScoreDeltas{ + nodeSuspicion: 12, + reporterReliability: 1, + ticketDeterioration: 12, + }, + }, + { + name: "timeout", + class: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_TIMEOUT_OR_NO_RESPONSE, + expect: storageTruthScoreDeltas{ + nodeSuspicion: 4, + reporterReliability: -1, + ticketDeterioration: 4, + }, + }, + { + name: "observer quorum fail", + class: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_OBSERVER_QUORUM_FAIL, + expect: storageTruthScoreDeltas{ + nodeSuspicion: 3, + reporterReliability: -3, + ticketDeterioration: 5, + }, + }, + { + name: "no eligible ticket", + class: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_NO_ELIGIBLE_TICKET, + expect: storageTruthScoreDeltas{ + nodeSuspicion: 0, + reporterReliability: 1, + ticketDeterioration: 0, + }, + }, + { + name: "invalid transcript", + class: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_INVALID_TRANSCRIPT, + expect: storageTruthScoreDeltas{ + nodeSuspicion: 0, + reporterReliability: -8, + ticketDeterioration: 0, + }, + }, + { + name: "recheck confirmed fail", + class: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_RECHECK_CONFIRMED_FAIL, + expect: storageTruthScoreDeltas{ + nodeSuspicion: 20, + reporterReliability: 3, + ticketDeterioration: 20, + }, + }, + { + name: "unknown", + class: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_UNSPECIFIED, + expect: storageTruthScoreDeltas{}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expect, storageTruthScoreDeltasForResultClass(tc.class)) + }) + } +} + +func TestDecayTowardZero(t *testing.T) { + tests := []struct { + name string + score int64 + decay int64 + elapsed uint64 + expect int64 + }{ + {name: "positive score", score: 10, decay: 2, elapsed: 3, expect: 4}, + {name: "positive clamps to zero", score: 5, decay: 3, elapsed: 3, expect: 0}, + {name: "negative score", score: -9, decay: 2, elapsed: 3, expect: -3}, + {name: "negative clamps to zero", score: -3, decay: 2, elapsed: 2, expect: 0}, + {name: "zero decay", score: 7, decay: 0, elapsed: 10, expect: 7}, + {name: "negative decay treated as disabled", score: 7, decay: -1, elapsed: 10, expect: 7}, + {name: "zero elapsed", score: 7, decay: 1, elapsed: 0, expect: 7}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expect, decayTowardZero(tc.score, tc.decay, tc.elapsed)) + }) + } +} + +func TestAddInt64Saturated(t *testing.T) { + require.Equal(t, int64(math.MaxInt64), addInt64Saturated(math.MaxInt64-1, 10)) + require.Equal(t, int64(math.MinInt64), addInt64Saturated(math.MinInt64+1, -10)) + require.Equal(t, int64(8), addInt64Saturated(3, 5)) +} + +func TestReporterTrustBandForScore(t *testing.T) { + params := types.DefaultParams().WithDefaults() + params.StorageTruthReporterReliabilityLowTrustThreshold = -20 + params.StorageTruthReporterReliabilityIneligibleThreshold = -50 + + require.Equal(t, types.ReporterTrustBand_REPORTER_TRUST_BAND_NORMAL, reporterTrustBandForScore(0, params)) + require.Equal(t, types.ReporterTrustBand_REPORTER_TRUST_BAND_LOW_TRUST, reporterTrustBandForScore(-20, params)) + require.Equal(t, types.ReporterTrustBand_REPORTER_TRUST_BAND_CHALLENGER_INELIGIBLE, reporterTrustBandForScore(-50, params)) +} + +func TestStorageTruthResultsContradict(t *testing.T) { + require.True(t, storageTruthResultsContradict( + types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_HASH_MISMATCH, + types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_PASS, + )) + require.True(t, storageTruthResultsContradict( + types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_PASS, + types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_TIMEOUT_OR_NO_RESPONSE, + )) + require.False(t, storageTruthResultsContradict( + types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_HASH_MISMATCH, + types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_TIMEOUT_OR_NO_RESPONSE, + )) +} + +func TestScaleInt64TowardZero(t *testing.T) { + require.Equal(t, int64(6), scaleInt64TowardZero(12, 50, 100)) + require.Equal(t, int64(-1), scaleInt64TowardZero(-3, 50, 100)) + require.Equal(t, int64(0), scaleInt64TowardZero(-3, 25, 100)) + require.Equal(t, int64(12), scaleInt64TowardZero(12, 100, 100)) +} diff --git a/x/audit/v1/keeper/storage_truth_state.go b/x/audit/v1/keeper/storage_truth_state.go index 645ed14d..bbd34651 100644 --- a/x/audit/v1/keeper/storage_truth_state.go +++ b/x/audit/v1/keeper/storage_truth_state.go @@ -192,3 +192,45 @@ func (k Keeper) GetAllHealOps(ctx sdk.Context) ([]types.HealOp, error) { } return healOps, nil } + +func (k Keeper) HasHealOpVerification(ctx sdk.Context, healOpID uint64, verifierSupernodeAccount string) bool { + store := k.kvStore(ctx) + return store.Has(types.HealOpVerificationKey(healOpID, verifierSupernodeAccount)) +} + +func (k Keeper) SetHealOpVerification(ctx sdk.Context, healOpID uint64, verifierSupernodeAccount string, verified bool) { + store := k.kvStore(ctx) + value := byte(0) + if verified { + value = 1 + } + store.Set(types.HealOpVerificationKey(healOpID, verifierSupernodeAccount), []byte{value}) +} + +func (k Keeper) GetHealOpVerification(ctx sdk.Context, healOpID uint64, verifierSupernodeAccount string) (bool, bool) { + store := k.kvStore(ctx) + bz := store.Get(types.HealOpVerificationKey(healOpID, verifierSupernodeAccount)) + if len(bz) == 0 { + return false, false + } + return bz[0] == 1, true +} + +func (k Keeper) GetAllHealOpVerifications(ctx sdk.Context, healOpID uint64) (map[string]bool, error) { + store := k.kvStore(ctx) + prefix := types.HealOpVerificationPrefix(healOpID) + it := store.Iterator(prefix, storetypes.PrefixEndBytes(prefix)) + defer it.Close() + + verifications := make(map[string]bool) + for ; it.Valid(); it.Next() { + key := it.Key() + if len(key) <= len(prefix) { + continue + } + verifier := string(key[len(prefix):]) + value := len(it.Value()) != 0 && it.Value()[0] == 1 + verifications[verifier] = value + } + return verifications, nil +} diff --git a/x/audit/v1/keeper/storage_truth_state_test.go b/x/audit/v1/keeper/storage_truth_state_test.go index cd5d4cc6..c19f9ad3 100644 --- a/x/audit/v1/keeper/storage_truth_state_test.go +++ b/x/audit/v1/keeper/storage_truth_state_test.go @@ -35,6 +35,8 @@ func TestReporterReliabilityStateRoundTrip(t *testing.T) { ReporterSupernodeAccount: "lumera1reporter0000000000000000000000000m09fa", ReliabilityScore: -5, LastUpdatedEpoch: 8, + TrustBand: types.ReporterTrustBand_REPORTER_TRUST_BAND_LOW_TRUST, + ContradictionCount: 3, } require.False(t, f.keeper.HasReporterReliabilityState(f.ctx, state.ReporterSupernodeAccount)) @@ -49,12 +51,19 @@ func TestTicketDeteriorationStateRoundTrip(t *testing.T) { f := initFixture(t) state := types.TicketDeteriorationState{ - TicketId: "ticket-1", - DeteriorationScore: 25, - LastUpdatedEpoch: 9, - ActiveHealOpId: 3, - ProbationUntilEpoch: 12, - LastHealEpoch: 10, + TicketId: "ticket-1", + DeteriorationScore: 25, + LastUpdatedEpoch: 9, + ActiveHealOpId: 3, + ProbationUntilEpoch: 12, + LastHealEpoch: 10, + LastFailureEpoch: 8, + RecentFailureEpochCount: 2, + ContradictionCount: 1, + LastTargetSupernodeAccount: "lumera1target0000000000000000000000000g6we", + LastReporterSupernodeAccount: "lumera1reporter0000000000000000000000000m09fa", + LastResultClass: types.StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_HASH_MISMATCH, + LastResultEpoch: 9, } require.False(t, f.keeper.HasTicketDeteriorationState(f.ctx, state.TicketId)) @@ -107,3 +116,33 @@ func TestHealOpAndNextIDRoundTrip(t *testing.T) { require.True(t, found) require.Equal(t, healOp, got) } + +func TestHealOpVerificationRoundTrip(t *testing.T) { + f := initFixture(t) + + healOpID := uint64(44) + verifierA := "lumera1verifiera00000000000000000000000h7v3e" + verifierB := "lumera1verifierb00000000000000000000000z9f3r" + + require.False(t, f.keeper.HasHealOpVerification(f.ctx, healOpID, verifierA)) + _, found := f.keeper.GetHealOpVerification(f.ctx, healOpID, verifierA) + require.False(t, found) + + f.keeper.SetHealOpVerification(f.ctx, healOpID, verifierA, true) + f.keeper.SetHealOpVerification(f.ctx, healOpID, verifierB, false) + + require.True(t, f.keeper.HasHealOpVerification(f.ctx, healOpID, verifierA)) + value, found := f.keeper.GetHealOpVerification(f.ctx, healOpID, verifierA) + require.True(t, found) + require.True(t, value) + + value, found = f.keeper.GetHealOpVerification(f.ctx, healOpID, verifierB) + require.True(t, found) + require.False(t, value) + + all, err := f.keeper.GetAllHealOpVerifications(f.ctx, healOpID) + require.NoError(t, err) + require.Len(t, all, 2) + require.True(t, all[verifierA]) + require.False(t, all[verifierB]) +} diff --git a/x/audit/v1/module/autocli.go b/x/audit/v1/module/autocli.go index a4ad152e..9c61c230 100644 --- a/x/audit/v1/module/autocli.go +++ b/x/audit/v1/module/autocli.go @@ -127,19 +127,19 @@ func (am AppModule) AutoCLIOptions() *autocliv1.ModuleOptions { { RpcMethod: "SubmitStorageRecheckEvidence", Use: "submit-storage-recheck-evidence [epoch-id] [challenged-supernode-account] [ticket-id]", - Short: "Submit storage-truth recheck evidence (foundation path; behavior implemented in a later PR)", + Short: "Submit storage-truth recheck evidence (reserved for the later LEP-6 recheck milestone)", PositionalArgs: []*autocliv1.PositionalArgDescriptor{{ProtoField: "epoch_id"}, {ProtoField: "challenged_supernode_account"}, {ProtoField: "ticket_id"}}, }, { RpcMethod: "ClaimHealComplete", Use: "claim-heal-complete [heal-op-id] [ticket-id] [heal-manifest-hash]", - Short: "Submit healer completion claim for a storage-truth heal op (implemented in a later PR)", + Short: "Submit healer completion claim for a storage-truth heal op", PositionalArgs: []*autocliv1.PositionalArgDescriptor{{ProtoField: "heal_op_id"}, {ProtoField: "ticket_id"}, {ProtoField: "heal_manifest_hash"}}, }, { RpcMethod: "SubmitHealVerification", Use: "submit-heal-verification [heal-op-id] [verified] [verification-hash]", - Short: "Submit verifier decision for a storage-truth heal op (implemented in a later PR)", + Short: "Submit verifier decision for a storage-truth heal op", PositionalArgs: []*autocliv1.PositionalArgDescriptor{{ProtoField: "heal_op_id"}, {ProtoField: "verified"}, {ProtoField: "verification_hash"}}, }, // this line is used by ignite scaffolding # autocli/tx diff --git a/x/audit/v1/types/audit.pb.go b/x/audit/v1/types/audit.pb.go index 2edb5520..b5c0e78b 100644 --- a/x/audit/v1/types/audit.pb.go +++ b/x/audit/v1/types/audit.pb.go @@ -159,6 +159,37 @@ func (StorageProofResultClass) EnumDescriptor() ([]byte, []int) { return fileDescriptor_0613fff850c07858, []int{3} } +type ReporterTrustBand int32 + +const ( + ReporterTrustBand_REPORTER_TRUST_BAND_UNSPECIFIED ReporterTrustBand = 0 + ReporterTrustBand_REPORTER_TRUST_BAND_NORMAL ReporterTrustBand = 1 + ReporterTrustBand_REPORTER_TRUST_BAND_LOW_TRUST ReporterTrustBand = 2 + ReporterTrustBand_REPORTER_TRUST_BAND_CHALLENGER_INELIGIBLE ReporterTrustBand = 3 +) + +var ReporterTrustBand_name = map[int32]string{ + 0: "REPORTER_TRUST_BAND_UNSPECIFIED", + 1: "REPORTER_TRUST_BAND_NORMAL", + 2: "REPORTER_TRUST_BAND_LOW_TRUST", + 3: "REPORTER_TRUST_BAND_CHALLENGER_INELIGIBLE", +} + +var ReporterTrustBand_value = map[string]int32{ + "REPORTER_TRUST_BAND_UNSPECIFIED": 0, + "REPORTER_TRUST_BAND_NORMAL": 1, + "REPORTER_TRUST_BAND_LOW_TRUST": 2, + "REPORTER_TRUST_BAND_CHALLENGER_INELIGIBLE": 3, +} + +func (x ReporterTrustBand) String() string { + return proto.EnumName(ReporterTrustBand_name, int32(x)) +} + +func (ReporterTrustBand) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_0613fff850c07858, []int{4} +} + type HealOpStatus int32 const ( @@ -196,7 +227,7 @@ func (x HealOpStatus) String() string { } func (HealOpStatus) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_0613fff850c07858, []int{4} + return fileDescriptor_0613fff850c07858, []int{5} } // HostReport is the Supernode's self-reported host metrics and counters for an epoch. @@ -513,9 +544,11 @@ func (m *NodeSuspicionState) GetLastUpdatedEpoch() uint64 { // ReporterReliabilityState is the persisted storage-truth reporter reliability snapshot. type ReporterReliabilityState struct { - ReporterSupernodeAccount string `protobuf:"bytes,1,opt,name=reporter_supernode_account,json=reporterSupernodeAccount,proto3" json:"reporter_supernode_account,omitempty"` - ReliabilityScore int64 `protobuf:"varint,2,opt,name=reliability_score,json=reliabilityScore,proto3" json:"reliability_score,omitempty"` - LastUpdatedEpoch uint64 `protobuf:"varint,3,opt,name=last_updated_epoch,json=lastUpdatedEpoch,proto3" json:"last_updated_epoch,omitempty"` + ReporterSupernodeAccount string `protobuf:"bytes,1,opt,name=reporter_supernode_account,json=reporterSupernodeAccount,proto3" json:"reporter_supernode_account,omitempty"` + ReliabilityScore int64 `protobuf:"varint,2,opt,name=reliability_score,json=reliabilityScore,proto3" json:"reliability_score,omitempty"` + LastUpdatedEpoch uint64 `protobuf:"varint,3,opt,name=last_updated_epoch,json=lastUpdatedEpoch,proto3" json:"last_updated_epoch,omitempty"` + TrustBand ReporterTrustBand `protobuf:"varint,4,opt,name=trust_band,json=trustBand,proto3,enum=lumera.audit.v1.ReporterTrustBand" json:"trust_band,omitempty"` + ContradictionCount uint64 `protobuf:"varint,5,opt,name=contradiction_count,json=contradictionCount,proto3" json:"contradiction_count,omitempty"` } func (m *ReporterReliabilityState) Reset() { *m = ReporterReliabilityState{} } @@ -572,14 +605,35 @@ func (m *ReporterReliabilityState) GetLastUpdatedEpoch() uint64 { return 0 } +func (m *ReporterReliabilityState) GetTrustBand() ReporterTrustBand { + if m != nil { + return m.TrustBand + } + return ReporterTrustBand_REPORTER_TRUST_BAND_UNSPECIFIED +} + +func (m *ReporterReliabilityState) GetContradictionCount() uint64 { + if m != nil { + return m.ContradictionCount + } + return 0 +} + // TicketDeteriorationState is the persisted storage-truth ticket deterioration snapshot. type TicketDeteriorationState struct { - TicketId string `protobuf:"bytes,1,opt,name=ticket_id,json=ticketId,proto3" json:"ticket_id,omitempty"` - DeteriorationScore int64 `protobuf:"varint,2,opt,name=deterioration_score,json=deteriorationScore,proto3" json:"deterioration_score,omitempty"` - LastUpdatedEpoch uint64 `protobuf:"varint,3,opt,name=last_updated_epoch,json=lastUpdatedEpoch,proto3" json:"last_updated_epoch,omitempty"` - ActiveHealOpId uint64 `protobuf:"varint,4,opt,name=active_heal_op_id,json=activeHealOpId,proto3" json:"active_heal_op_id,omitempty"` - ProbationUntilEpoch uint64 `protobuf:"varint,5,opt,name=probation_until_epoch,json=probationUntilEpoch,proto3" json:"probation_until_epoch,omitempty"` - LastHealEpoch uint64 `protobuf:"varint,6,opt,name=last_heal_epoch,json=lastHealEpoch,proto3" json:"last_heal_epoch,omitempty"` + TicketId string `protobuf:"bytes,1,opt,name=ticket_id,json=ticketId,proto3" json:"ticket_id,omitempty"` + DeteriorationScore int64 `protobuf:"varint,2,opt,name=deterioration_score,json=deteriorationScore,proto3" json:"deterioration_score,omitempty"` + LastUpdatedEpoch uint64 `protobuf:"varint,3,opt,name=last_updated_epoch,json=lastUpdatedEpoch,proto3" json:"last_updated_epoch,omitempty"` + ActiveHealOpId uint64 `protobuf:"varint,4,opt,name=active_heal_op_id,json=activeHealOpId,proto3" json:"active_heal_op_id,omitempty"` + ProbationUntilEpoch uint64 `protobuf:"varint,5,opt,name=probation_until_epoch,json=probationUntilEpoch,proto3" json:"probation_until_epoch,omitempty"` + LastHealEpoch uint64 `protobuf:"varint,6,opt,name=last_heal_epoch,json=lastHealEpoch,proto3" json:"last_heal_epoch,omitempty"` + LastFailureEpoch uint64 `protobuf:"varint,7,opt,name=last_failure_epoch,json=lastFailureEpoch,proto3" json:"last_failure_epoch,omitempty"` + RecentFailureEpochCount uint32 `protobuf:"varint,8,opt,name=recent_failure_epoch_count,json=recentFailureEpochCount,proto3" json:"recent_failure_epoch_count,omitempty"` + ContradictionCount uint64 `protobuf:"varint,9,opt,name=contradiction_count,json=contradictionCount,proto3" json:"contradiction_count,omitempty"` + LastTargetSupernodeAccount string `protobuf:"bytes,10,opt,name=last_target_supernode_account,json=lastTargetSupernodeAccount,proto3" json:"last_target_supernode_account,omitempty"` + LastReporterSupernodeAccount string `protobuf:"bytes,11,opt,name=last_reporter_supernode_account,json=lastReporterSupernodeAccount,proto3" json:"last_reporter_supernode_account,omitempty"` + LastResultClass StorageProofResultClass `protobuf:"varint,12,opt,name=last_result_class,json=lastResultClass,proto3,enum=lumera.audit.v1.StorageProofResultClass" json:"last_result_class,omitempty"` + LastResultEpoch uint64 `protobuf:"varint,13,opt,name=last_result_epoch,json=lastResultEpoch,proto3" json:"last_result_epoch,omitempty"` } func (m *TicketDeteriorationState) Reset() { *m = TicketDeteriorationState{} } @@ -657,6 +711,55 @@ func (m *TicketDeteriorationState) GetLastHealEpoch() uint64 { return 0 } +func (m *TicketDeteriorationState) GetLastFailureEpoch() uint64 { + if m != nil { + return m.LastFailureEpoch + } + return 0 +} + +func (m *TicketDeteriorationState) GetRecentFailureEpochCount() uint32 { + if m != nil { + return m.RecentFailureEpochCount + } + return 0 +} + +func (m *TicketDeteriorationState) GetContradictionCount() uint64 { + if m != nil { + return m.ContradictionCount + } + return 0 +} + +func (m *TicketDeteriorationState) GetLastTargetSupernodeAccount() string { + if m != nil { + return m.LastTargetSupernodeAccount + } + return "" +} + +func (m *TicketDeteriorationState) GetLastReporterSupernodeAccount() string { + if m != nil { + return m.LastReporterSupernodeAccount + } + return "" +} + +func (m *TicketDeteriorationState) GetLastResultClass() StorageProofResultClass { + if m != nil { + return m.LastResultClass + } + return StorageProofResultClass_STORAGE_PROOF_RESULT_CLASS_UNSPECIFIED +} + +func (m *TicketDeteriorationState) GetLastResultEpoch() uint64 { + if m != nil { + return m.LastResultEpoch + } + return 0 +} + // HealOp is the chain-tracked storage-truth healing operation state. type HealOp struct { HealOpId uint64 `protobuf:"varint,1,opt,name=heal_op_id,json=healOpId,proto3" json:"heal_op_id,omitempty"` @@ -872,6 +975,7 @@ func init() { proto.RegisterEnum("lumera.audit.v1.StorageProofBucketType", StorageProofBucketType_name, StorageProofBucketType_value) proto.RegisterEnum("lumera.audit.v1.StorageProofArtifactClass", StorageProofArtifactClass_name, StorageProofArtifactClass_value) proto.RegisterEnum("lumera.audit.v1.StorageProofResultClass", StorageProofResultClass_name, StorageProofResultClass_value) + proto.RegisterEnum("lumera.audit.v1.ReporterTrustBand", ReporterTrustBand_name, ReporterTrustBand_value) proto.RegisterEnum("lumera.audit.v1.HealOpStatus", HealOpStatus_name, HealOpStatus_value) proto.RegisterType((*HostReport)(nil), "lumera.audit.v1.HostReport") proto.RegisterType((*StorageChallengeObservation)(nil), "lumera.audit.v1.StorageChallengeObservation") @@ -886,106 +990,120 @@ func init() { func init() { proto.RegisterFile("lumera/audit/v1/audit.proto", fileDescriptor_0613fff850c07858) } var fileDescriptor_0613fff850c07858 = []byte{ - // 1583 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x57, 0x4f, 0x6f, 0xdb, 0xc8, - 0x15, 0x37, 0x25, 0xd9, 0xb1, 0x9f, 0x62, 0x99, 0x1e, 0xe7, 0x8f, 0x6c, 0x67, 0x15, 0xc7, 0x69, - 0x12, 0x47, 0xcd, 0xc6, 0x8d, 0x17, 0x7b, 0xea, 0x89, 0x92, 0xe8, 0x88, 0xb5, 0x2c, 0x6a, 0x87, - 0x54, 0x76, 0xd3, 0xa2, 0x18, 0xd0, 0xe4, 0x44, 0x22, 0x42, 0x8b, 0x04, 0x87, 0x32, 0xea, 0x0f, - 0x51, 0x60, 0xcf, 0x05, 0x7a, 0xe8, 0xad, 0xd7, 0x02, 0xed, 0x77, 0xd8, 0x63, 0xd0, 0x53, 0x0f, - 0x45, 0x51, 0x24, 0xfd, 0x20, 0xc5, 0xcc, 0x90, 0xfa, 0x6b, 0xcb, 0x41, 0xd0, 0x5e, 0x04, 0xcd, - 0xef, 0xf7, 0x7b, 0x6f, 0xde, 0x9b, 0x79, 0xf3, 0x66, 0x08, 0xbb, 0xc1, 0xf0, 0x9c, 0xc6, 0xce, - 0xa1, 0x33, 0xf4, 0xfc, 0xe4, 0xf0, 0xe2, 0x95, 0xfc, 0xf3, 0x32, 0x8a, 0xc3, 0x24, 0x44, 0x1b, - 0x92, 0x7c, 0x29, 0xb1, 0x8b, 0x57, 0x3b, 0x9b, 0xce, 0xb9, 0x3f, 0x08, 0x0f, 0xc5, 0xaf, 0xd4, - 0xec, 0x6c, 0xbb, 0x21, 0x3b, 0x0f, 0x19, 0x11, 0xa3, 0x43, 0x39, 0x48, 0xa9, 0x3b, 0xbd, 0xb0, - 0x17, 0x4a, 0x9c, 0xff, 0x93, 0xe8, 0xfe, 0x8f, 0x39, 0x80, 0x66, 0xc8, 0x12, 0x4c, 0xa3, 0x30, - 0x4e, 0x50, 0x15, 0x36, 0xdd, 0x68, 0x48, 0x86, 0xcc, 0xe9, 0x51, 0x12, 0xd1, 0xd8, 0xa5, 0x83, - 0xa4, 0xac, 0xec, 0x29, 0x07, 0x0a, 0xde, 0x70, 0xa3, 0x61, 0x97, 0xe3, 0x1d, 0x09, 0x73, 0xed, - 0x39, 0x3d, 0x9f, 0xd1, 0xe6, 0xa4, 0xf6, 0x9c, 0x9e, 0x4f, 0x69, 0x5f, 0x00, 0xf2, 0x7c, 0xf6, - 0x7e, 0x46, 0x9c, 0x17, 0x62, 0x95, 0x33, 0x53, 0xea, 0x5f, 0xc1, 0x96, 0x3f, 0x38, 0x0b, 0x87, - 0x03, 0x8f, 0xf0, 0xa8, 0x08, 0x4b, 0x9c, 0x84, 0xb2, 0x72, 0x61, 0x2f, 0x7f, 0x50, 0x3a, 0xda, - 0x79, 0x39, 0xb3, 0x0e, 0x2f, 0x3b, 0x61, 0x9c, 0x58, 0x5c, 0x82, 0x37, 0x53, 0xb3, 0x11, 0xc2, - 0xd0, 0x2f, 0xe0, 0xce, 0x3b, 0xc7, 0x0f, 0xa8, 0x47, 0x1c, 0x37, 0xf1, 0xc3, 0x01, 0x23, 0x6e, - 0x38, 0x1c, 0x24, 0xe5, 0xe5, 0x3d, 0xe5, 0x60, 0x1d, 0x23, 0xc9, 0x69, 0x92, 0xaa, 0x73, 0x66, - 0xff, 0x2f, 0x0a, 0xec, 0x5a, 0x49, 0x18, 0x3b, 0x3d, 0x5a, 0xef, 0x3b, 0x41, 0x40, 0x07, 0x3d, - 0x6a, 0x9e, 0x31, 0x1a, 0x5f, 0x38, 0x5c, 0x85, 0xba, 0x50, 0x4e, 0x9c, 0xb8, 0x47, 0x13, 0xc2, - 0x86, 0x11, 0x8d, 0x07, 0xa1, 0x47, 0x89, 0xe3, 0x4a, 0xaf, 0x7c, 0xa9, 0xd6, 0x6a, 0xbb, 0x7f, - 0xff, 0xeb, 0xd7, 0xf7, 0xd3, 0xc5, 0xd7, 0x5c, 0x57, 0xf3, 0xbc, 0x98, 0x32, 0x66, 0x25, 0xb1, - 0x3f, 0xe8, 0xe1, 0x7b, 0xd2, 0xd8, 0xca, 0x6c, 0x35, 0x69, 0x8a, 0x7e, 0x09, 0xc5, 0xc9, 0x64, - 0x73, 0x37, 0x26, 0x0b, 0xd1, 0x28, 0xcb, 0xfd, 0x8f, 0x05, 0x40, 0x69, 0xcc, 0x9d, 0x38, 0x0c, - 0xdf, 0x61, 0xca, 0x86, 0x41, 0xf2, 0xff, 0x0a, 0xf5, 0xb7, 0xf0, 0xc0, 0xcd, 0x56, 0x26, 0xbe, - 0xc2, 0x75, 0xee, 0x66, 0xd7, 0x3b, 0x63, 0x07, 0x73, 0xee, 0x77, 0x61, 0x2d, 0xf1, 0xdd, 0xf7, - 0x34, 0x21, 0xbe, 0x27, 0x6a, 0x64, 0x0d, 0xaf, 0x4a, 0xc0, 0xf0, 0x50, 0x13, 0x8a, 0x67, 0x43, - 0x41, 0x26, 0x97, 0x11, 0x2d, 0x17, 0xf6, 0x94, 0x83, 0xd2, 0xd1, 0xb3, 0xb9, 0x65, 0x9a, 0x5c, - 0x8c, 0x9a, 0xd0, 0xdb, 0x97, 0x11, 0xc5, 0x70, 0x36, 0xfa, 0x8f, 0xbe, 0x83, 0x92, 0x13, 0x27, - 0xfe, 0x3b, 0xc7, 0x4d, 0x88, 0x1b, 0x38, 0x8c, 0x89, 0x9a, 0x28, 0x1d, 0x55, 0x17, 0x3a, 0xd3, - 0x52, 0x93, 0x3a, 0xb7, 0xc0, 0xeb, 0xce, 0xe4, 0x10, 0x3d, 0x07, 0x75, 0xe4, 0x32, 0x8c, 0x3d, - 0x7f, 0xe0, 0x04, 0xe5, 0x15, 0x51, 0x68, 0x1b, 0x19, 0x6e, 0x4a, 0x18, 0x3d, 0x82, 0xdb, 0x23, - 0xe9, 0x7b, 0x7a, 0x59, 0xbe, 0x25, 0xf2, 0x2c, 0x66, 0xd8, 0x09, 0xbd, 0x44, 0x27, 0x70, 0x3b, - 0x16, 0xfb, 0x98, 0x86, 0xb7, 0x2a, 0xc2, 0x3b, 0x58, 0x18, 0x9e, 0xdc, 0x78, 0x19, 0x5c, 0x31, - 0x1e, 0x0f, 0xd0, 0x33, 0xd8, 0x48, 0x62, 0x67, 0xc0, 0xdc, 0xd8, 0x8f, 0x12, 0xd2, 0x77, 0x58, - 0xbf, 0xbc, 0x26, 0xa6, 0x2c, 0x8d, 0xe1, 0xa6, 0xc3, 0xfa, 0xa8, 0x0c, 0xb7, 0x3c, 0x9a, 0x38, - 0x7e, 0xc0, 0xca, 0x20, 0x04, 0xd9, 0x70, 0xff, 0x6f, 0x0a, 0xa0, 0x76, 0xe8, 0x51, 0x6b, 0xc8, - 0x22, 0xdf, 0xf5, 0xc3, 0x81, 0x28, 0x3e, 0xd4, 0x84, 0xcd, 0x2f, 0xaa, 0x2e, 0x95, 0xcd, 0x6e, - 0xfc, 0x33, 0xd8, 0x60, 0x99, 0x6f, 0xc2, 0xdc, 0x30, 0xa6, 0xa2, 0x94, 0xf2, 0xb8, 0x34, 0x82, - 0x2d, 0x8e, 0xf2, 0x76, 0x12, 0x38, 0x2c, 0x21, 0xc3, 0xc8, 0x73, 0x12, 0xea, 0x11, 0x1a, 0x85, - 0x6e, 0x5f, 0x94, 0x4a, 0x01, 0xab, 0x9c, 0xe9, 0x4a, 0x42, 0xe7, 0xf8, 0xfe, 0x07, 0x05, 0xca, - 0xb2, 0xbf, 0xd1, 0x18, 0xd3, 0xc0, 0x77, 0xce, 0xfc, 0xc0, 0x4f, 0x2e, 0x65, 0xf4, 0x6f, 0x61, - 0x27, 0x4e, 0xb9, 0x2f, 0x3b, 0x24, 0xe5, 0xcc, 0x7c, 0xae, 0x8e, 0x7f, 0x0e, 0x9b, 0xf1, 0x78, - 0xba, 0xa9, 0x84, 0xd4, 0x09, 0xe2, 0x4b, 0x52, 0xfa, 0x63, 0x0e, 0xca, 0xb6, 0x38, 0x12, 0x0d, - 0x9a, 0xd0, 0xd8, 0x0f, 0x63, 0xd1, 0x9b, 0x64, 0x4a, 0x53, 0xe7, 0x47, 0x99, 0x39, 0x3f, 0x87, - 0xb0, 0xe5, 0x4d, 0x9a, 0x4c, 0x85, 0x85, 0xa6, 0xa8, 0x2f, 0x08, 0x0c, 0x3d, 0x87, 0x4d, 0xde, - 0x67, 0x2f, 0x28, 0xe9, 0x53, 0x27, 0x20, 0x61, 0xc4, 0x63, 0x28, 0x08, 0x71, 0x49, 0x12, 0x4d, - 0xea, 0x04, 0x66, 0x64, 0x78, 0xe8, 0x08, 0xee, 0x46, 0x71, 0x78, 0x26, 0xa3, 0x18, 0x0e, 0x12, - 0x3f, 0x48, 0x7d, 0x2f, 0x0b, 0xf9, 0xd6, 0x88, 0xec, 0x72, 0x4e, 0xba, 0x7f, 0x0a, 0x1b, 0x22, - 0x18, 0xe1, 0x5c, 0xaa, 0x57, 0x84, 0x7a, 0x9d, 0xc3, 0xdc, 0xb5, 0x5c, 0x9f, 0xdf, 0x17, 0x60, - 0x45, 0x4e, 0x84, 0x1e, 0x00, 0x4c, 0x84, 0xa2, 0x08, 0xf5, 0x6a, 0x3f, 0x0b, 0x62, 0x6a, 0xad, - 0x72, 0x33, 0x6b, 0xf5, 0x02, 0x10, 0x73, 0xfb, 0xd4, 0x1b, 0x06, 0x59, 0xde, 0x59, 0x47, 0x2a, - 0x60, 0x75, 0xc4, 0x88, 0x19, 0x0d, 0x8f, 0x37, 0x5b, 0xee, 0xf6, 0xca, 0x3a, 0x2a, 0x7c, 0x46, - 0xb3, 0x95, 0xc6, 0x73, 0x55, 0xf4, 0x1b, 0xd8, 0xbd, 0xa0, 0xb1, 0xff, 0xce, 0xbf, 0xca, 0x31, - 0xef, 0x59, 0xf9, 0x9b, 0x3c, 0x6f, 0x67, 0xf6, 0xb3, 0xbe, 0x19, 0xfa, 0x16, 0x56, 0xf8, 0x7d, - 0x33, 0x64, 0x62, 0x19, 0x4b, 0x47, 0x5f, 0xcd, 0x35, 0x17, 0xb9, 0x8a, 0x96, 0x10, 0xe1, 0x54, - 0x8c, 0x9e, 0x40, 0xc9, 0x8d, 0xa9, 0x28, 0x87, 0x3e, 0xf5, 0x7b, 0xfd, 0x44, 0xb4, 0xaf, 0x02, - 0x5e, 0x4f, 0xd1, 0xa6, 0x00, 0xb9, 0x2c, 0xab, 0x9a, 0x54, 0xb6, 0x2a, 0x65, 0x29, 0x9a, 0xca, - 0xaa, 0xb0, 0xe9, 0x51, 0xc7, 0x0b, 0xfc, 0x01, 0x1d, 0xaf, 0xf2, 0x9a, 0x50, 0x6e, 0x64, 0x44, - 0xb6, 0xc8, 0x0f, 0x21, 0xed, 0x6a, 0xb2, 0x85, 0xc9, 0x0e, 0x05, 0x12, 0x12, 0xed, 0xeb, 0x0e, - 0x2c, 0x0f, 0x42, 0x7e, 0x81, 0x16, 0x05, 0x25, 0x07, 0xfb, 0x7f, 0xce, 0x43, 0x51, 0xb8, 0x48, - 0xdf, 0x39, 0xff, 0xbb, 0x9e, 0xb5, 0x0d, 0xab, 0xa3, 0x98, 0x73, 0x22, 0xe6, 0x5b, 0x34, 0x8d, - 0xf5, 0x31, 0xac, 0xcb, 0xde, 0x90, 0x65, 0x9f, 0x17, 0x87, 0xec, 0xb6, 0x04, 0xd3, 0xe4, 0x6b, - 0x50, 0xec, 0x87, 0x2c, 0x21, 0x12, 0x14, 0x85, 0x52, 0x3c, 0xda, 0x9d, 0xdf, 0x86, 0xd1, 0x1b, - 0xad, 0x56, 0xf8, 0xe9, 0x5f, 0x0f, 0x97, 0x30, 0xf4, 0xc7, 0xaf, 0xb6, 0x18, 0x2a, 0x4c, 0xde, - 0x01, 0x64, 0x74, 0xad, 0x92, 0x70, 0xfc, 0x64, 0x91, 0x55, 0x52, 0x3c, 0x7a, 0x71, 0xdd, 0xd5, - 0x71, 0xd5, 0x3b, 0x07, 0x3f, 0x60, 0xd7, 0x93, 0x0c, 0x7d, 0x0f, 0x77, 0xb3, 0x39, 0x23, 0x7e, - 0xf1, 0x10, 0xb9, 0x07, 0xbc, 0x90, 0xf8, 0x54, 0x8f, 0x3f, 0xe3, 0x96, 0xc2, 0x5b, 0x6c, 0x0e, - 0x63, 0x55, 0x13, 0xd6, 0x46, 0x6f, 0x1c, 0x74, 0x0f, 0x50, 0xc7, 0xc4, 0x36, 0xb1, 0x6c, 0xcd, - 0xd6, 0x49, 0xb7, 0x7d, 0xd2, 0x36, 0xbf, 0x6f, 0xab, 0x4b, 0x68, 0x0b, 0x36, 0x26, 0x70, 0xb3, - 0xa3, 0xb7, 0x55, 0x05, 0xdd, 0x85, 0xcd, 0x09, 0xb0, 0xde, 0x32, 0x2d, 0xbd, 0xa1, 0xe6, 0xaa, - 0xff, 0x54, 0xe0, 0xde, 0xd5, 0xcf, 0x01, 0xf4, 0x1c, 0x9e, 0x58, 0xb6, 0x89, 0xb5, 0xd7, 0x3a, - 0xe9, 0x60, 0xd3, 0x3c, 0x26, 0xb5, 0x6e, 0xfd, 0x44, 0xb7, 0x89, 0xfd, 0xb6, 0xc3, 0x67, 0xb3, - 0x3a, 0x7a, 0xdd, 0x38, 0x36, 0xf4, 0x86, 0xba, 0x84, 0x7e, 0x06, 0x7b, 0xd7, 0x4b, 0xb1, 0x5e, - 0xd7, 0xdb, 0xb6, 0xaa, 0xa0, 0x47, 0xf0, 0xd5, 0xf5, 0x2a, 0xb3, 0xd5, 0x50, 0x73, 0xe8, 0x19, - 0x3c, 0xbe, 0x5e, 0xd2, 0xc1, 0x66, 0x4d, 0xb3, 0x0d, 0xb3, 0xad, 0xe6, 0xd1, 0x13, 0x78, 0xb4, - 0x70, 0xc6, 0xa6, 0x5e, 0x3f, 0x51, 0x0b, 0xd5, 0x3f, 0x28, 0xb0, 0x7d, 0xed, 0x03, 0x05, 0xbd, - 0x80, 0x83, 0x69, 0x27, 0x1a, 0xb6, 0x8d, 0x63, 0xad, 0x6e, 0x93, 0x7a, 0x4b, 0xb3, 0xac, 0x99, - 0x24, 0x9f, 0xc2, 0xfe, 0x42, 0xb5, 0xd1, 0x6e, 0xe8, 0x3f, 0xa8, 0xca, 0x7c, 0x0e, 0x33, 0x3a, - 0xeb, 0xed, 0x69, 0xcd, 0x6c, 0xa9, 0xb9, 0xea, 0x9f, 0xf2, 0x70, 0xff, 0x9a, 0xe7, 0x09, 0xaa, - 0xc2, 0xd3, 0x69, 0x27, 0x58, 0xb7, 0xba, 0xad, 0xab, 0x03, 0x7b, 0x0c, 0x0f, 0x17, 0x68, 0x3b, - 0x9a, 0x65, 0xa9, 0xca, 0x7c, 0xae, 0x53, 0xa2, 0xa6, 0x66, 0x35, 0xc9, 0xa9, 0x61, 0x9d, 0x6a, - 0x76, 0xbd, 0xa9, 0xe6, 0xd0, 0xb7, 0xf0, 0x6a, 0x81, 0xda, 0x36, 0x4e, 0x75, 0xb3, 0x6b, 0x13, - 0x13, 0x93, 0xb6, 0xc9, 0xa9, 0x8e, 0xd9, 0xb6, 0x74, 0x35, 0x8f, 0xbe, 0x81, 0xc3, 0x05, 0x66, - 0x66, 0xcd, 0xd2, 0xf1, 0x1b, 0x1d, 0x93, 0xef, 0xba, 0x26, 0xee, 0x9e, 0x92, 0x63, 0xcd, 0x68, - 0xa9, 0x05, 0xf4, 0x0a, 0xbe, 0x5e, 0x60, 0xd4, 0x36, 0x89, 0xde, 0x32, 0x5e, 0x1b, 0xb5, 0x96, - 0x4e, 0x6c, 0x83, 0xef, 0xb1, 0xba, 0x7c, 0x83, 0x89, 0xd1, 0x7e, 0xa3, 0xb5, 0x8c, 0x06, 0xb1, - 0xb1, 0xd6, 0xb6, 0xea, 0xd8, 0xe8, 0xd8, 0xea, 0xca, 0x0d, 0x19, 0xa5, 0x15, 0x43, 0xea, 0x66, - 0xfb, 0xd8, 0xc0, 0xa7, 0x7a, 0x43, 0x06, 0x77, 0xab, 0xfa, 0x1f, 0x05, 0x6e, 0x4f, 0x76, 0x79, - 0x54, 0x81, 0x9d, 0xa6, 0xae, 0xb5, 0x88, 0xd9, 0x11, 0x47, 0xa9, 0x3b, 0xbb, 0x19, 0x0f, 0xa0, - 0x3c, 0xc3, 0x5b, 0xf5, 0xa6, 0xde, 0xe8, 0xb6, 0xf4, 0x86, 0xaa, 0x5c, 0x61, 0x6d, 0xb4, 0x79, - 0x3c, 0xaf, 0xb1, 0x6e, 0x59, 0x6a, 0x0e, 0xed, 0x43, 0x65, 0x86, 0xe7, 0x43, 0x1d, 0x13, 0xac, - 0xf3, 0xd3, 0xab, 0x37, 0xd4, 0x3c, 0xda, 0x85, 0xfb, 0x33, 0x9a, 0x37, 0x3a, 0x96, 0xd3, 0x17, - 0xd0, 0x36, 0xdc, 0x9d, 0x21, 0x79, 0x22, 0x7a, 0x43, 0x5d, 0x46, 0x3b, 0x70, 0x6f, 0x86, 0xd2, - 0x7f, 0xe8, 0x18, 0x58, 0x6f, 0xa8, 0x2b, 0xb5, 0xea, 0x4f, 0x1f, 0x2b, 0xca, 0x87, 0x8f, 0x15, - 0xe5, 0xdf, 0x1f, 0x2b, 0xca, 0x8f, 0x9f, 0x2a, 0x4b, 0x1f, 0x3e, 0x55, 0x96, 0xfe, 0xf1, 0xa9, - 0xb2, 0xf4, 0x6b, 0xf5, 0x77, 0xe3, 0x0f, 0x6e, 0xfe, 0x51, 0xc1, 0xce, 0x56, 0xc4, 0xc7, 0xf1, - 0x37, 0xff, 0x0d, 0x00, 0x00, 0xff, 0xff, 0x04, 0xc2, 0x7f, 0x30, 0x90, 0x0f, 0x00, 0x00, + // 1794 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x58, 0x4b, 0x6f, 0xdb, 0xca, + 0x15, 0x36, 0x25, 0xf9, 0x75, 0xfc, 0xa2, 0xc6, 0x79, 0xc8, 0x8f, 0x38, 0x8e, 0xd2, 0x24, 0x8e, + 0x9a, 0xc4, 0x8d, 0x2f, 0xee, 0xea, 0xae, 0x28, 0x89, 0x8e, 0x58, 0xcb, 0xa4, 0xee, 0x90, 0x4a, + 0x6e, 0x5a, 0xb4, 0x03, 0x8a, 0x9c, 0x58, 0x44, 0x64, 0x52, 0x20, 0x29, 0xa3, 0xfe, 0x11, 0x05, + 0xee, 0xba, 0xbb, 0xee, 0x8a, 0xee, 0x0a, 0xb4, 0xff, 0xa0, 0x8b, 0xbb, 0x0c, 0xba, 0xea, 0xa2, + 0x28, 0x8a, 0xa4, 0xff, 0xa3, 0xc5, 0xcc, 0x90, 0x7a, 0x4b, 0x4e, 0x83, 0xde, 0x8d, 0xc1, 0x39, + 0xdf, 0x77, 0xce, 0x9c, 0x39, 0x73, 0x1e, 0x63, 0xc1, 0x5e, 0xa7, 0x77, 0x49, 0x43, 0xfb, 0xd8, + 0xee, 0xb9, 0x5e, 0x7c, 0x7c, 0xf5, 0x52, 0x7c, 0xbc, 0xe8, 0x86, 0x41, 0x1c, 0xa0, 0x2d, 0x01, + 0xbe, 0x10, 0xb2, 0xab, 0x97, 0xbb, 0x79, 0xfb, 0xd2, 0xf3, 0x83, 0x63, 0xfe, 0x57, 0x70, 0x76, + 0x77, 0x9c, 0x20, 0xba, 0x0c, 0x22, 0xc2, 0x57, 0xc7, 0x62, 0x91, 0x40, 0xb7, 0x2e, 0x82, 0x8b, + 0x40, 0xc8, 0xd9, 0x97, 0x90, 0x16, 0xbf, 0xcf, 0x00, 0xd4, 0x82, 0x28, 0xc6, 0xb4, 0x1b, 0x84, + 0x31, 0x2a, 0x41, 0xde, 0xe9, 0xf6, 0x48, 0x2f, 0xb2, 0x2f, 0x28, 0xe9, 0xd2, 0xd0, 0xa1, 0x7e, + 0x5c, 0x90, 0x0e, 0xa5, 0x23, 0x09, 0x6f, 0x39, 0xdd, 0x5e, 0x93, 0xc9, 0x1b, 0x42, 0xcc, 0xb8, + 0x97, 0xf4, 0x72, 0x8c, 0x9b, 0x11, 0xdc, 0x4b, 0x7a, 0x39, 0xc2, 0x7d, 0x06, 0xc8, 0xf5, 0xa2, + 0xf7, 0x63, 0xe4, 0x2c, 0x27, 0xcb, 0x0c, 0x19, 0x61, 0xff, 0x1c, 0xb6, 0x3d, 0xbf, 0x15, 0xf4, + 0x7c, 0x97, 0x30, 0xaf, 0x48, 0x14, 0xdb, 0x31, 0x8d, 0x0a, 0xb9, 0xc3, 0xec, 0xd1, 0xe6, 0xc9, + 0xee, 0x8b, 0xb1, 0x38, 0xbc, 0x68, 0x04, 0x61, 0x6c, 0x32, 0x0a, 0xce, 0x27, 0x6a, 0x7d, 0x49, + 0x84, 0x7e, 0x06, 0xb7, 0xde, 0xd9, 0x5e, 0x87, 0xba, 0xc4, 0x76, 0x62, 0x2f, 0xf0, 0x23, 0xe2, + 0x04, 0x3d, 0x3f, 0x2e, 0x2c, 0x1e, 0x4a, 0x47, 0x1b, 0x18, 0x09, 0x4c, 0x11, 0x50, 0x85, 0x21, + 0xc5, 0x3f, 0x49, 0xb0, 0x67, 0xc6, 0x41, 0x68, 0x5f, 0xd0, 0x4a, 0xdb, 0xee, 0x74, 0xa8, 0x7f, + 0x41, 0x8d, 0x56, 0x44, 0xc3, 0x2b, 0x9b, 0xb1, 0x50, 0x13, 0x0a, 0xb1, 0x1d, 0x5e, 0xd0, 0x98, + 0x44, 0xbd, 0x2e, 0x0d, 0xfd, 0xc0, 0xa5, 0xc4, 0x76, 0x84, 0x55, 0x16, 0xaa, 0xd5, 0xf2, 0xde, + 0xdf, 0xfe, 0xfc, 0xfc, 0x6e, 0x12, 0x7c, 0xc5, 0x71, 0x14, 0xd7, 0x0d, 0x69, 0x14, 0x99, 0x71, + 0xe8, 0xf9, 0x17, 0xf8, 0x8e, 0x50, 0x36, 0x53, 0x5d, 0x45, 0xa8, 0xa2, 0x6f, 0x60, 0x6d, 0xf8, + 0xb0, 0x99, 0x1b, 0x0f, 0x0b, 0xdd, 0xfe, 0x29, 0x8b, 0x1f, 0x73, 0x80, 0x12, 0x9f, 0x1b, 0x61, + 0x10, 0xbc, 0xc3, 0x34, 0xea, 0x75, 0xe2, 0x1f, 0xcb, 0xd5, 0x5f, 0xc1, 0xbe, 0x93, 0x46, 0x26, + 0x9c, 0x62, 0x3a, 0x73, 0xb3, 0xe9, 0xdd, 0x81, 0x81, 0x09, 0xf3, 0x7b, 0xb0, 0x1a, 0x7b, 0xce, + 0x7b, 0x1a, 0x13, 0xcf, 0xe5, 0x39, 0xb2, 0x8a, 0x57, 0x84, 0x40, 0x73, 0x51, 0x0d, 0xd6, 0x5a, + 0x3d, 0x0e, 0xc6, 0xd7, 0x5d, 0x5a, 0xc8, 0x1d, 0x4a, 0x47, 0x9b, 0x27, 0x4f, 0x26, 0xc2, 0x34, + 0x1c, 0x8c, 0x32, 0xe7, 0x5b, 0xd7, 0x5d, 0x8a, 0xa1, 0xd5, 0xff, 0x46, 0xdf, 0xc2, 0xa6, 0x1d, + 0xc6, 0xde, 0x3b, 0xdb, 0x89, 0x89, 0xd3, 0xb1, 0xa3, 0x88, 0xe7, 0xc4, 0xe6, 0x49, 0x69, 0xae, + 0x31, 0x25, 0x51, 0xa9, 0x30, 0x0d, 0xbc, 0x61, 0x0f, 0x2f, 0xd1, 0x53, 0x90, 0xfb, 0x26, 0x83, + 0xd0, 0xf5, 0x7c, 0xbb, 0x53, 0x58, 0xe2, 0x89, 0xb6, 0x95, 0xca, 0x0d, 0x21, 0x46, 0x0f, 0x60, + 0xbd, 0x4f, 0x7d, 0x4f, 0xaf, 0x0b, 0xcb, 0xfc, 0x9c, 0x6b, 0xa9, 0xec, 0x8c, 0x5e, 0xa3, 0x33, + 0x58, 0x0f, 0xf9, 0x3d, 0x26, 0xee, 0xad, 0x70, 0xf7, 0x8e, 0xe6, 0xba, 0x27, 0x2e, 0x5e, 0x38, + 0xb7, 0x16, 0x0e, 0x16, 0xe8, 0x09, 0x6c, 0xc5, 0xa1, 0xed, 0x47, 0x4e, 0xe8, 0x75, 0x63, 0xd2, + 0xb6, 0xa3, 0x76, 0x61, 0x95, 0x6f, 0xb9, 0x39, 0x10, 0xd7, 0xec, 0xa8, 0x8d, 0x0a, 0xb0, 0xec, + 0xd2, 0xd8, 0xf6, 0x3a, 0x51, 0x01, 0x38, 0x21, 0x5d, 0x16, 0xff, 0x22, 0x01, 0xd2, 0x03, 0x97, + 0x9a, 0xbd, 0xa8, 0xeb, 0x39, 0x5e, 0xe0, 0xf3, 0xe4, 0x43, 0x35, 0xc8, 0x7f, 0x51, 0x76, 0xc9, + 0xd1, 0xf8, 0xc5, 0x3f, 0x81, 0xad, 0x28, 0xb5, 0x4d, 0x22, 0x27, 0x08, 0x29, 0x4f, 0xa5, 0x2c, + 0xde, 0xec, 0x8b, 0x4d, 0x26, 0x65, 0xed, 0xa4, 0x63, 0x47, 0x31, 0xe9, 0x75, 0x5d, 0x3b, 0xa6, + 0x2e, 0xa1, 0xdd, 0xc0, 0x69, 0xf3, 0x54, 0xc9, 0x61, 0x99, 0x21, 0x4d, 0x01, 0xa8, 0x4c, 0x5e, + 0xfc, 0x6b, 0x06, 0x0a, 0xa2, 0xbf, 0xd1, 0x10, 0xd3, 0x8e, 0x67, 0xb7, 0xbc, 0x8e, 0x17, 0x5f, + 0x0b, 0xef, 0xdf, 0xc2, 0x6e, 0x98, 0x60, 0x5f, 0x56, 0x24, 0x85, 0x54, 0x7d, 0x22, 0x8f, 0x7f, + 0x0a, 0xf9, 0x70, 0xb0, 0xdd, 0xc8, 0x81, 0xe4, 0x21, 0xe0, 0x0b, 0x8e, 0x84, 0x14, 0x80, 0x38, + 0xec, 0x45, 0x31, 0x69, 0xd9, 0xbe, 0x9b, 0x14, 0x41, 0x71, 0x22, 0x31, 0xd2, 0x43, 0x5b, 0x8c, + 0x5a, 0xb6, 0x7d, 0x17, 0xaf, 0xc6, 0xe9, 0x27, 0x3a, 0x86, 0x6d, 0x27, 0xf0, 0xe3, 0xd0, 0x76, + 0x3d, 0xde, 0xfd, 0x86, 0xfa, 0x62, 0x0e, 0xa3, 0x11, 0x48, 0xf4, 0xc5, 0xff, 0x2c, 0x42, 0xc1, + 0xe2, 0x65, 0x58, 0xa5, 0x31, 0x0d, 0xbd, 0x20, 0xe4, 0xfd, 0x50, 0x84, 0x71, 0xa4, 0x66, 0xa5, + 0xb1, 0x9a, 0x3d, 0x86, 0x6d, 0x77, 0x58, 0x65, 0x24, 0x14, 0x68, 0x04, 0xfa, 0x92, 0x60, 0x3c, + 0x85, 0x3c, 0xeb, 0xed, 0x57, 0x94, 0xb4, 0xa9, 0xdd, 0x21, 0x41, 0x97, 0xf9, 0x90, 0xe3, 0xe4, + 0x4d, 0x01, 0xd4, 0xa8, 0xdd, 0x31, 0xba, 0x9a, 0x8b, 0x4e, 0xe0, 0x76, 0x37, 0x0c, 0x5a, 0xc2, + 0x8b, 0x9e, 0x1f, 0x7b, 0x9d, 0xc4, 0xb6, 0x38, 0xf6, 0x76, 0x1f, 0x6c, 0x32, 0x4c, 0x98, 0x7f, + 0x0c, 0x5b, 0xdc, 0x19, 0x6e, 0x5c, 0xb0, 0x97, 0x38, 0x7b, 0x83, 0x89, 0x99, 0x69, 0xc1, 0x4b, + 0x9d, 0x66, 0x23, 0xa5, 0x17, 0xd2, 0x84, 0xba, 0x3c, 0x70, 0xfa, 0x54, 0x00, 0x82, 0xfd, 0x0d, + 0xcb, 0x3b, 0x36, 0xed, 0x46, 0xf9, 0xc9, 0x2d, 0xac, 0xf0, 0xa6, 0x71, 0x57, 0x30, 0x86, 0xf5, + 0xf8, 0x55, 0xcc, 0xba, 0xbb, 0xd5, 0x59, 0x77, 0x87, 0x7e, 0x0d, 0xf7, 0xb8, 0x6f, 0x33, 0xa7, + 0x01, 0x7c, 0x46, 0xcb, 0x66, 0x16, 0xac, 0xe9, 0x13, 0xa1, 0x05, 0xf7, 0xb9, 0xfd, 0x39, 0xa5, + 0xb4, 0x76, 0xf3, 0x0e, 0xfb, 0xcc, 0x06, 0x9e, 0x55, 0x4e, 0x16, 0xe4, 0x93, 0x3d, 0x86, 0x7a, + 0xe2, 0xfa, 0xff, 0xd8, 0x13, 0xb7, 0xc4, 0x16, 0x83, 0xbe, 0x58, 0x1a, 0xb5, 0x2a, 0x2e, 0x6d, + 0x83, 0x07, 0x72, 0x88, 0x2b, 0x1a, 0xc9, 0x6f, 0x73, 0xb0, 0x24, 0x52, 0x09, 0xed, 0x03, 0x0c, + 0x25, 0x9b, 0xc4, 0xf9, 0x2b, 0xed, 0x34, 0xcd, 0x46, 0xaa, 0x21, 0x33, 0x56, 0x0d, 0xcf, 0x00, + 0x45, 0x4e, 0x9b, 0xba, 0xbd, 0x4e, 0x9a, 0xd9, 0xe9, 0x9c, 0xcb, 0x61, 0xb9, 0x8f, 0xf0, 0x1d, + 0x35, 0x97, 0x8d, 0x70, 0x66, 0x76, 0x6a, 0x48, 0x73, 0x9f, 0x31, 0xc2, 0x85, 0xf2, 0x44, 0x30, + 0x7f, 0x09, 0x7b, 0x57, 0x34, 0xf4, 0xde, 0x79, 0xd3, 0x0c, 0xb3, 0x49, 0x98, 0xbd, 0xc9, 0xf2, + 0x4e, 0xaa, 0x3f, 0x6e, 0x3b, 0x42, 0x5f, 0xc3, 0x12, 0x7b, 0xc5, 0xf4, 0x22, 0x5e, 0x28, 0x9b, + 0x27, 0xf7, 0x26, 0xae, 0x47, 0x44, 0xd1, 0xe4, 0x24, 0x9c, 0x90, 0xd1, 0x23, 0xd8, 0x74, 0x42, + 0xca, 0x0b, 0xbe, 0x4d, 0xbd, 0x8b, 0x76, 0x9c, 0x14, 0xcf, 0x46, 0x22, 0xad, 0x71, 0x21, 0xa3, + 0xa5, 0x7d, 0x21, 0xa1, 0xad, 0x08, 0x5a, 0x22, 0x4d, 0x68, 0x25, 0xc8, 0xbb, 0xd4, 0x76, 0x3b, + 0x9e, 0x4f, 0x07, 0x51, 0x16, 0x15, 0xb2, 0x95, 0x02, 0x69, 0x90, 0xef, 0x43, 0x32, 0x2b, 0xc5, + 0x60, 0x14, 0x73, 0x0f, 0x84, 0x88, 0x0f, 0xc5, 0x5b, 0xb0, 0xe8, 0x07, 0xec, 0x59, 0xc6, 0xb3, + 0x18, 0x8b, 0x45, 0xf1, 0x0f, 0x59, 0x58, 0xe3, 0x26, 0x92, 0xd7, 0xf3, 0xff, 0x6f, 0x12, 0xee, + 0xc0, 0x4a, 0xdf, 0xe7, 0x0c, 0xf7, 0x79, 0x99, 0x26, 0xbe, 0x3e, 0x84, 0x0d, 0x51, 0x65, 0xe9, + 0xe9, 0xb3, 0xbc, 0x8d, 0xae, 0x0b, 0x61, 0x72, 0xf8, 0x32, 0xac, 0xb5, 0x83, 0x7e, 0x3d, 0xf2, + 0x44, 0x59, 0x3b, 0xd9, 0x9b, 0xbc, 0x86, 0xfe, 0xcb, 0xbf, 0x9c, 0xfb, 0xe1, 0x9f, 0xf7, 0x17, + 0x30, 0xb4, 0x07, 0xff, 0x0b, 0x84, 0x70, 0x10, 0x89, 0x2a, 0x22, 0xfd, 0xc7, 0x1a, 0x09, 0x06, + 0x0f, 0x61, 0x91, 0x25, 0x6b, 0x27, 0xcf, 0x66, 0x15, 0xdf, 0xb4, 0xd7, 0x33, 0xde, 0x8f, 0x66, + 0x83, 0x11, 0x7a, 0x03, 0xb7, 0xd3, 0x3d, 0xbb, 0xac, 0x74, 0x93, 0xb2, 0x64, 0x89, 0xc4, 0xb6, + 0x7a, 0xf8, 0x19, 0x75, 0x8e, 0xb7, 0xa3, 0x09, 0x59, 0x54, 0x32, 0x60, 0xb5, 0xff, 0x72, 0x46, + 0x77, 0x00, 0x35, 0x0c, 0x6c, 0x11, 0xd3, 0x52, 0x2c, 0x95, 0x34, 0xf5, 0x33, 0xdd, 0x78, 0xa3, + 0xcb, 0x0b, 0x68, 0x1b, 0xb6, 0x86, 0xe4, 0x46, 0x43, 0xd5, 0x65, 0x09, 0xdd, 0x86, 0xfc, 0x90, + 0xb0, 0x52, 0x37, 0x4c, 0xb5, 0x2a, 0x67, 0x4a, 0xff, 0x90, 0xe0, 0xce, 0xf4, 0x47, 0x26, 0x7a, + 0x0a, 0x8f, 0x4c, 0xcb, 0xc0, 0xca, 0x2b, 0x95, 0x34, 0xb0, 0x61, 0x9c, 0x92, 0x72, 0xb3, 0x72, + 0xa6, 0x5a, 0xc4, 0x7a, 0xdb, 0x60, 0xbb, 0x99, 0x0d, 0xb5, 0xa2, 0x9d, 0x6a, 0x6a, 0x55, 0x5e, + 0x40, 0x3f, 0x81, 0xc3, 0xd9, 0x54, 0xac, 0x56, 0x54, 0xdd, 0x92, 0x25, 0xf4, 0x00, 0xee, 0xcd, + 0x66, 0x19, 0xf5, 0xaa, 0x9c, 0x41, 0x4f, 0xe0, 0xe1, 0x6c, 0x4a, 0x03, 0x1b, 0x65, 0xc5, 0xd2, + 0x0c, 0x5d, 0xce, 0xa2, 0x47, 0xf0, 0x60, 0xee, 0x8e, 0x35, 0xb5, 0x72, 0x26, 0xe7, 0x4a, 0xbf, + 0x93, 0x60, 0x67, 0xe6, 0xb3, 0x17, 0x3d, 0x83, 0xa3, 0x51, 0x23, 0x0a, 0xb6, 0xb4, 0x53, 0xa5, + 0x62, 0x91, 0x4a, 0x5d, 0x31, 0xcd, 0xb1, 0x43, 0x3e, 0x86, 0xe2, 0x5c, 0xb6, 0xa6, 0x57, 0xd5, + 0xef, 0x64, 0x69, 0xf2, 0x0c, 0x63, 0x3c, 0xf3, 0xed, 0x79, 0xd9, 0xa8, 0xcb, 0x99, 0xd2, 0xef, + 0xb3, 0x70, 0x77, 0x46, 0x83, 0x47, 0x25, 0x78, 0x3c, 0x6a, 0x04, 0xab, 0x66, 0xb3, 0x3e, 0xdd, + 0xb1, 0x87, 0x70, 0x7f, 0x0e, 0xb7, 0xa1, 0x98, 0xa6, 0x2c, 0x4d, 0x9e, 0x75, 0x84, 0x54, 0x53, + 0xcc, 0x1a, 0x39, 0xd7, 0xcc, 0x73, 0xc5, 0xaa, 0xd4, 0xe4, 0x0c, 0xfa, 0x1a, 0x5e, 0xce, 0x61, + 0x5b, 0xda, 0xb9, 0x6a, 0x34, 0x2d, 0x62, 0x60, 0xa2, 0x1b, 0x0c, 0x6a, 0x18, 0xba, 0xa9, 0xca, + 0x59, 0xf4, 0x15, 0x1c, 0xcf, 0x51, 0x33, 0xca, 0xa6, 0x8a, 0x5f, 0xab, 0x98, 0x7c, 0xdb, 0x34, + 0x70, 0xf3, 0x9c, 0x9c, 0x2a, 0x5a, 0x5d, 0xce, 0xa1, 0x97, 0xf0, 0x7c, 0x8e, 0x92, 0x6e, 0x10, + 0xb5, 0xae, 0xbd, 0xd2, 0xca, 0x75, 0x95, 0x58, 0x1a, 0xbb, 0x63, 0x79, 0xf1, 0x06, 0x15, 0x4d, + 0x7f, 0xad, 0xd4, 0xb5, 0x2a, 0xb1, 0xb0, 0xa2, 0x9b, 0x15, 0xac, 0x35, 0x2c, 0x79, 0xe9, 0x86, + 0x13, 0x25, 0x19, 0x43, 0x2a, 0x86, 0x7e, 0xaa, 0xe1, 0x73, 0xb5, 0x2a, 0x9c, 0x5b, 0x2e, 0xfd, + 0x51, 0x82, 0xfc, 0xc4, 0xfb, 0x93, 0x45, 0x1c, 0xab, 0xac, 0x9c, 0x54, 0x4c, 0x2c, 0xdc, 0x34, + 0x2d, 0x52, 0x56, 0xf4, 0xea, 0xd8, 0xb5, 0x1c, 0xc0, 0xee, 0x34, 0x92, 0x6e, 0xe0, 0x73, 0xa5, + 0x2e, 0xca, 0x61, 0x1a, 0x5e, 0x37, 0xde, 0x88, 0xa5, 0x9c, 0x41, 0xcf, 0xe1, 0xe9, 0x34, 0x4a, + 0xa5, 0xa6, 0xd4, 0xeb, 0xaa, 0xfe, 0x4a, 0xc5, 0x44, 0xd3, 0xd3, 0xe8, 0xc8, 0xd9, 0xd2, 0xbf, + 0x25, 0x58, 0x1f, 0x1e, 0x49, 0xcc, 0x85, 0x9a, 0xaa, 0xd4, 0x89, 0xd1, 0xe0, 0x75, 0xdf, 0x1c, + 0xcf, 0x9c, 0x7d, 0x28, 0x8c, 0xe1, 0x66, 0xa5, 0xa6, 0x56, 0x9b, 0x75, 0xb5, 0x2a, 0x4b, 0x53, + 0xb4, 0x35, 0x9d, 0x05, 0xef, 0x15, 0x56, 0x4d, 0x53, 0xce, 0xa0, 0x22, 0x1c, 0x8c, 0xe1, 0x6c, + 0xa9, 0x62, 0x92, 0xf8, 0x5c, 0x95, 0xb3, 0x68, 0x0f, 0xee, 0x8e, 0x71, 0x5e, 0xab, 0x58, 0x6c, + 0x9f, 0x43, 0x3b, 0x70, 0x7b, 0x0c, 0x64, 0x51, 0x57, 0xab, 0xf2, 0x22, 0xda, 0x85, 0x3b, 0x63, + 0x90, 0xfa, 0x5d, 0x43, 0xc3, 0x6a, 0x55, 0x5e, 0x2a, 0x97, 0x7e, 0xf8, 0x78, 0x20, 0x7d, 0xf8, + 0x78, 0x20, 0xfd, 0xeb, 0xe3, 0x81, 0xf4, 0xfd, 0xa7, 0x83, 0x85, 0x0f, 0x9f, 0x0e, 0x16, 0xfe, + 0xfe, 0xe9, 0x60, 0xe1, 0x17, 0xf2, 0x6f, 0x06, 0xbf, 0x39, 0xb1, 0xff, 0xab, 0xa3, 0xd6, 0x12, + 0xff, 0x7d, 0xe8, 0xab, 0xff, 0x06, 0x00, 0x00, 0xff, 0xff, 0xd6, 0x58, 0x07, 0x4b, 0x93, 0x12, + 0x00, 0x00, } func (m *HostReport) Marshal() (dAtA []byte, err error) { @@ -1245,6 +1363,16 @@ func (m *ReporterReliabilityState) MarshalToSizedBuffer(dAtA []byte) (int, error _ = i var l int _ = l + if m.ContradictionCount != 0 { + i = encodeVarintAudit(dAtA, i, uint64(m.ContradictionCount)) + i-- + dAtA[i] = 0x28 + } + if m.TrustBand != 0 { + i = encodeVarintAudit(dAtA, i, uint64(m.TrustBand)) + i-- + dAtA[i] = 0x20 + } if m.LastUpdatedEpoch != 0 { i = encodeVarintAudit(dAtA, i, uint64(m.LastUpdatedEpoch)) i-- @@ -1285,6 +1413,45 @@ func (m *TicketDeteriorationState) MarshalToSizedBuffer(dAtA []byte) (int, error _ = i var l int _ = l + if m.LastResultEpoch != 0 { + i = encodeVarintAudit(dAtA, i, uint64(m.LastResultEpoch)) + i-- + dAtA[i] = 0x68 + } + if m.LastResultClass != 0 { + i = encodeVarintAudit(dAtA, i, uint64(m.LastResultClass)) + i-- + dAtA[i] = 0x60 + } + if len(m.LastReporterSupernodeAccount) > 0 { + i -= len(m.LastReporterSupernodeAccount) + copy(dAtA[i:], m.LastReporterSupernodeAccount) + i = encodeVarintAudit(dAtA, i, uint64(len(m.LastReporterSupernodeAccount))) + i-- + dAtA[i] = 0x5a + } + if len(m.LastTargetSupernodeAccount) > 0 { + i -= len(m.LastTargetSupernodeAccount) + copy(dAtA[i:], m.LastTargetSupernodeAccount) + i = encodeVarintAudit(dAtA, i, uint64(len(m.LastTargetSupernodeAccount))) + i-- + dAtA[i] = 0x52 + } + if m.ContradictionCount != 0 { + i = encodeVarintAudit(dAtA, i, uint64(m.ContradictionCount)) + i-- + dAtA[i] = 0x48 + } + if m.RecentFailureEpochCount != 0 { + i = encodeVarintAudit(dAtA, i, uint64(m.RecentFailureEpochCount)) + i-- + dAtA[i] = 0x40 + } + if m.LastFailureEpoch != 0 { + i = encodeVarintAudit(dAtA, i, uint64(m.LastFailureEpoch)) + i-- + dAtA[i] = 0x38 + } if m.LastHealEpoch != 0 { i = encodeVarintAudit(dAtA, i, uint64(m.LastHealEpoch)) i-- @@ -1627,6 +1794,12 @@ func (m *ReporterReliabilityState) Size() (n int) { if m.LastUpdatedEpoch != 0 { n += 1 + sovAudit(uint64(m.LastUpdatedEpoch)) } + if m.TrustBand != 0 { + n += 1 + sovAudit(uint64(m.TrustBand)) + } + if m.ContradictionCount != 0 { + n += 1 + sovAudit(uint64(m.ContradictionCount)) + } return n } @@ -1655,6 +1828,29 @@ func (m *TicketDeteriorationState) Size() (n int) { if m.LastHealEpoch != 0 { n += 1 + sovAudit(uint64(m.LastHealEpoch)) } + if m.LastFailureEpoch != 0 { + n += 1 + sovAudit(uint64(m.LastFailureEpoch)) + } + if m.RecentFailureEpochCount != 0 { + n += 1 + sovAudit(uint64(m.RecentFailureEpochCount)) + } + if m.ContradictionCount != 0 { + n += 1 + sovAudit(uint64(m.ContradictionCount)) + } + l = len(m.LastTargetSupernodeAccount) + if l > 0 { + n += 1 + l + sovAudit(uint64(l)) + } + l = len(m.LastReporterSupernodeAccount) + if l > 0 { + n += 1 + l + sovAudit(uint64(l)) + } + if m.LastResultClass != 0 { + n += 1 + sovAudit(uint64(m.LastResultClass)) + } + if m.LastResultEpoch != 0 { + n += 1 + sovAudit(uint64(m.LastResultEpoch)) + } return n } @@ -2605,6 +2801,44 @@ func (m *ReporterReliabilityState) Unmarshal(dAtA []byte) error { break } } + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field TrustBand", wireType) + } + m.TrustBand = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAudit + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.TrustBand |= ReporterTrustBand(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ContradictionCount", wireType) + } + m.ContradictionCount = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAudit + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ContradictionCount |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipAudit(dAtA[iNdEx:]) @@ -2782,6 +3016,165 @@ func (m *TicketDeteriorationState) Unmarshal(dAtA []byte) error { break } } + case 7: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field LastFailureEpoch", wireType) + } + m.LastFailureEpoch = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAudit + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.LastFailureEpoch |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 8: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field RecentFailureEpochCount", wireType) + } + m.RecentFailureEpochCount = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAudit + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.RecentFailureEpochCount |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 9: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ContradictionCount", wireType) + } + m.ContradictionCount = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAudit + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ContradictionCount |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 10: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LastTargetSupernodeAccount", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAudit + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthAudit + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthAudit + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.LastTargetSupernodeAccount = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 11: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LastReporterSupernodeAccount", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAudit + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthAudit + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthAudit + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.LastReporterSupernodeAccount = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 12: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field LastResultClass", wireType) + } + m.LastResultClass = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAudit + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.LastResultClass |= StorageProofResultClass(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 13: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field LastResultEpoch", wireType) + } + m.LastResultEpoch = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAudit + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.LastResultEpoch |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipAudit(dAtA[iNdEx:]) diff --git a/x/audit/v1/types/errors.go b/x/audit/v1/types/errors.go index 66787752..f9344003 100644 --- a/x/audit/v1/types/errors.go +++ b/x/audit/v1/types/errors.go @@ -13,6 +13,12 @@ var ( ErrReporterNotFound = errorsmod.Register(ModuleName, 7, "reporter supernode not found") ErrInvalidReporterState = errorsmod.Register(ModuleName, 8, "invalid reporter state") ErrInvalidStorageProofs = errorsmod.Register(ModuleName, 9, "invalid storage proof results") + ErrInvalidRecheckEvidence = errorsmod.Register(ModuleName, 10, "invalid storage recheck evidence") + ErrHealOpNotFound = errorsmod.Register(ModuleName, 11, "heal op not found") + ErrHealOpUnauthorized = errorsmod.Register(ModuleName, 12, "heal op unauthorized actor") + ErrHealOpInvalidState = errorsmod.Register(ModuleName, 13, "heal op invalid state transition") + ErrHealOpTicketMismatch = errorsmod.Register(ModuleName, 14, "heal op ticket mismatch") + ErrHealVerificationExists = errorsmod.Register(ModuleName, 15, "heal verification already submitted") ErrInvalidEvidenceType = errorsmod.Register(ModuleName, 1101, "invalid evidence type") ErrInvalidMetadata = errorsmod.Register(ModuleName, 1102, "invalid evidence metadata") diff --git a/x/audit/v1/types/events.go b/x/audit/v1/types/events.go new file mode 100644 index 00000000..8e3ffc5b --- /dev/null +++ b/x/audit/v1/types/events.go @@ -0,0 +1,33 @@ +package types + +// Event types and attributes for storage-truth score updates. +const ( + EventTypeStorageTruthScoreUpdated = "storage_truth_score_updated" + EventTypeHealOpScheduled = "storage_truth_heal_op_scheduled" + EventTypeHealOpExpired = "storage_truth_heal_op_expired" + EventTypeHealOpHealerReported = "storage_truth_heal_op_healer_reported" + EventTypeHealOpVerified = "storage_truth_heal_op_verified" + EventTypeHealOpFailed = "storage_truth_heal_op_failed" + EventTypeStorageRecheckEvidence = "storage_truth_recheck_evidence_submitted" + + AttributeKeyEpochID = "epoch_id" + AttributeKeyReporterSupernodeAccount = "reporter_supernode_account" + AttributeKeyTargetSupernodeAccount = "target_supernode_account" + AttributeKeyTicketID = "ticket_id" + AttributeKeyHealOpID = "heal_op_id" + AttributeKeyVerifierSupernodeAccount = "verifier_supernode_account" + AttributeKeyHealerSupernodeAccount = "healer_supernode_account" + AttributeKeyVerified = "verified" + AttributeKeyVerificationHash = "verification_hash" + AttributeKeyTranscriptHash = "transcript_hash" + AttributeKeyDeadlineEpochID = "deadline_epoch_id" + AttributeKeyResultClass = "result_class" + AttributeKeyBucketType = "bucket_type" + AttributeKeyNodeSuspicionScore = "node_suspicion_score" + AttributeKeyReporterReliabilityScore = "reporter_reliability_score" + AttributeKeyTicketDeteriorationScore = "ticket_deterioration_score" + AttributeKeyReporterTrustBand = "reporter_trust_band" + AttributeKeyRepeatedFailureCount = "repeated_failure_count" + AttributeKeyContradictionDetected = "contradiction_detected" + AttributeKeyContradictedReporter = "contradicted_reporter" +) diff --git a/x/audit/v1/types/keys.go b/x/audit/v1/types/keys.go index ef45bb09..b42837fb 100644 --- a/x/audit/v1/types/keys.go +++ b/x/audit/v1/types/keys.go @@ -79,6 +79,7 @@ var ( // - HealOpKey: "st/ho/" + u64be(heal_op_id) // - HealOpByTicketIndexKey: "st/hot/" + ticket_id + 0x00 + u64be(heal_op_id) // - HealOpByStatusIndexKey: "st/hos/" + u32be(status) + u64be(heal_op_id) + // - HealOpVerificationKey: "st/hov/" + u64be(heal_op_id) + "/" + verifier_supernode_account // - NextHealOpIDKey: "st/next_ho_id" nodeSuspicionStatePrefix = []byte("st/ns/") reporterReliabilityStatePrefix = []byte("st/rr/") @@ -86,6 +87,7 @@ var ( healOpPrefix = []byte("st/ho/") healOpByTicketIndexPrefix = []byte("st/hot/") healOpByStatusIndexPrefix = []byte("st/hos/") + healOpVerificationPrefix = []byte("st/hov/") nextHealOpIDKey = []byte("st/next_ho_id") ) @@ -377,3 +379,20 @@ func HealOpByStatusIndexPrefix(status HealOpStatus) []byte { func NextHealOpIDKey() []byte { return nextHealOpIDKey } + +func HealOpVerificationKey(healOpID uint64, verifierSupernodeAccount string) []byte { + key := make([]byte, 0, len(healOpVerificationPrefix)+8+1+len(verifierSupernodeAccount)) // "st/hov/" + u64be(heal_op_id) + "/" + verifier + key = append(key, healOpVerificationPrefix...) + key = binary.BigEndian.AppendUint64(key, healOpID) + key = append(key, '/') + key = append(key, verifierSupernodeAccount...) + return key +} + +func HealOpVerificationPrefix(healOpID uint64) []byte { + key := make([]byte, 0, len(healOpVerificationPrefix)+8+1) // "st/hov/" + u64be(heal_op_id) + "/" + key = append(key, healOpVerificationPrefix...) + key = binary.BigEndian.AppendUint64(key, healOpID) + key = append(key, '/') + return key +}