Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
383 changes: 283 additions & 100 deletions api/uexecutor/v1/genesis.pulsar.go

Large diffs are not rendered by default.

3,208 changes: 3,208 additions & 0 deletions api/uexecutor/v1/pending.pulsar.go

Large diffs are not rendered by default.

2,121 changes: 1,726 additions & 395 deletions api/uexecutor/v1/query.pulsar.go

Large diffs are not rendered by default.

43 changes: 43 additions & 0 deletions api/uexecutor/v1/query_grpc.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 9 additions & 3 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -754,12 +754,18 @@ func NewChainApp(
app.UexecutorKeeper,
)

app.UvalidatorKeeper.SetHooks(
uvalidatorkeeper.NewMultiUValidatorHooks(
// uvalidator exposes two distinct hook surfaces, both registered in one call:
// - Validator: validator-lifecycle events (consumed by x/utss + x/uexecutor)
// - Ballot: ballot-terminal events (consumed by x/uexecutor only,
// for the F-2026-16642 variant audit-trail cleanup of
// PendingInbounds → ExpiredInbounds)
app.UvalidatorKeeper.SetHooks(uvalidatorkeeper.Hooks{
Validator: uvalidatorkeeper.NewMultiUValidatorHooks(
app.UtssKeeper.Hooks(),
uexecutorkeeper.NewUValidatorHooks(app.UexecutorKeeper),
),
)
Ballot: uexecutorkeeper.NewBallotHooks(app.UexecutorKeeper),
})

app.EVMKeeper.SetHooks(uexecutorkeeper.NewEVMHooks(app.UexecutorKeeper))

Expand Down
16 changes: 14 additions & 2 deletions proto/uexecutor/v1/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "amino/amino.proto";
import "uexecutor/v1/types.proto";
import "uexecutor/v1/gas_price.proto";
import "uexecutor/v1/chain_meta.proto";
import "uexecutor/v1/pending.proto";
import "uexecutor/v1/query.proto";

option go_package = "github.com/pushchain/push-chain-node/x/uexecutor/types";
Expand Down Expand Up @@ -33,8 +34,13 @@ message GenesisState {
// Params defines all the parameters of the module.
Params params = 1 [(gogoproto.nullable) = false];

// pending_inbounds are the keys from the PendingInbounds KeySet.
repeated string pending_inbounds = 2;
// pending_inbounds are entries from the PendingInbounds index.
// Per-variant audit-trail entries — see plan-pending-inbound-cleanup.md.
// Field 2 was previously `repeated string` (legacy KeySet keys); the
// shape change is non-breaking for in-flight state because the
// collection moved to a fresh prefix and the old prefix entries are
// dropped at upgrade time by a one-shot migration.
repeated PendingInboundEntry pending_inbounds = 2 [(gogoproto.nullable) = false];

// universal_txs are key-value pairs from the UniversalTx Map.
repeated UniversalTxEntry universal_txs = 3 [(gogoproto.nullable) = false];
Expand All @@ -54,4 +60,10 @@ message GenesisState {

// pending_outbounds are entries from the PendingOutbounds index.
repeated PendingOutboundEntry pending_outbounds = 8 [(gogoproto.nullable) = false];

// expired_inbounds are entries from the ExpiredInbounds index.
// Per-variant audit-trail of inbounds whose ballots all reached
// EXPIRED/REJECTED without producing a UniversalTx. Consumed by the
// future escape-hatch refund flow.
repeated ExpiredInboundEntry expired_inbounds = 9 [(gogoproto.nullable) = false];
}
107 changes: 107 additions & 0 deletions proto/uexecutor/v1/pending.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
syntax = "proto3";
package uexecutor.v1;

import "gogoproto/gogo.proto";
import "uexecutor/v1/types.proto";
import "uvalidator/v1/ballot.proto";

option go_package = "github.com/pushchain/push-chain-node/x/uexecutor/types";

// ========================================================================
// Per-variant audit-trail types for PendingInbounds and PendingOutbounds.
//
// Background: when validators observe the same source-chain inbound or
// destination-chain outbound, byte-level differences in their submitted
// payloads (different decoded fields, formatting, etc.) produce different
// ballot keys and therefore separate ballots. The variant types below
// preserve, on-chain, which validators voted what payload — so operators
// (and the future escape-hatch refund flow) can investigate stuck items.
//
// See:
// plan-pending-inbound-cleanup.md
// plan-pending-outbound-cleanup.md
// ========================================================================

// InboundVariant captures one Inbound payload variant submitted by one
// or more validators against a single logical inbound event (identified
// by the UTX key = sha256(source_chain:tx_hash:log_index)). Multiple
// variants may exist for the same UTX key when validators marshal
// slightly different bytes for the same logical event.
message InboundVariant {
option (gogoproto.equal) = true;

// ballot_id == hex(marshal(Inbound)) — the ballot key used by uvalidator.
string ballot_id = 1;
// The full Inbound payload exactly as voted (the bytes that produced
// this ballot_id).
Inbound inbound = 2;
// Validator addresses (bech32) that voted on this exact variant.
repeated string voters = 3;
// Block height of the first vote on this variant.
uint64 first_voted_at_height = 4;
// Block height of the most recent vote on this variant.
uint64 last_voted_at_height = 5;
// Terminal status of this variant's ballot. PENDING while in-flight.
// Populated by the uvalidator BallotHooks terminal callback.
uvalidator.v1.BallotStatus terminal_status = 6;
}

// PendingInboundEntry tracks all ballot variants for a single logical
// inbound event (identified by utx_key). Created by the first vote
// (RecordInboundVote). Removed only when ALL variants reach a terminal
// state. If any variant ended PASSED, the existing post-finalization
// path produces the UniversalTx. If ALL variants ended EXPIRED/REJECTED,
// the entry is moved to ExpiredInbounds.
message PendingInboundEntry {
option (gogoproto.equal) = true;

// sha256(source_chain:tx_hash:log_index) — same key used by
// GetInboundUniversalTxKey and the UniversalTx record (when it
// eventually exists).
string utx_key = 1;
repeated InboundVariant variants = 2 [(gogoproto.nullable) = false];
// Block height when this entry was created (first vote on any variant).
uint64 created_at_height = 3;
}

// ExpiredInboundEntry preserves the full per-variant audit trail of an
// inbound that failed to reach quorum on any variant. Consumed by the
// future escape-hatch refund flow.
message ExpiredInboundEntry {
option (gogoproto.equal) = true;

string utx_key = 1;
// Each variant carries its terminal_status (EXPIRED or REJECTED).
repeated InboundVariant variants = 2 [(gogoproto.nullable) = false];
// Block height when the entry was moved here (i.e. when the LAST
// variant's ballot reached a terminal state).
uint64 expired_at_height = 3;
}

// OutboundObservationVariant captures one OutboundObservation variant
// submitted by one or more validators against a single outbound (the
// outbound itself is deterministic — chain-side at outbound creation —
// so all variants share the same outbound_id). Multiple variants exist
// when validators see different destination-chain results (different
// success/tx_hash/error_msg/gas_fee_used).
//
// NOTE: Unlike inbound variants, outbound variants do not carry a
// terminal_status field. Outbound PendingOutbounds entries persist
// until validators reach consensus (existing inline removal in
// msg_vote_outbound.go on PASSED). Operators investigate stuck
// outbounds by correlating each variant's ballot_id with the
// uvalidator ballot status separately.
message OutboundObservationVariant {
option (gogoproto.equal) = true;

// ballot_id == sha256(utxId:outboundId:marshal(observedTx)).
string ballot_id = 1;
// The exact OutboundObservation that produced this ballot_id.
OutboundObservation observed_tx = 2 [(gogoproto.nullable) = false];
// Validator addresses (bech32) that voted on this exact variant.
repeated string voters = 3;
// Block height of the first vote on this variant.
uint64 first_voted_at_height = 4;
// Block height of the most recent vote on this variant.
uint64 last_voted_at_height = 5;
}
32 changes: 30 additions & 2 deletions proto/uexecutor/v1/query.proto
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
syntax = "proto3";
package uexecutor.v1;

import "gogoproto/gogo.proto";
import "google/api/annotations.proto";
import "uexecutor/v1/types.proto";
import "uexecutor/v1/gas_price.proto";
import "uexecutor/v1/chain_meta.proto";
import "uexecutor/v1/pending.proto";
import "cosmos/base/query/v1beta1/pagination.proto";

option go_package = "github.com/pushchain/push-chain-node/x/uexecutor/types";
Expand Down Expand Up @@ -60,6 +62,13 @@ service Query {
rpc AllPendingOutbounds(QueryAllPendingOutboundsRequest) returns (QueryAllPendingOutboundsResponse) {
option (google.api.http).get = "/uexecutor/v1/pending_outbounds";
}

// Queries all expired inbound entries (per-variant audit trail of
// inbounds whose ballots all reached EXPIRED/REJECTED without producing
// a UniversalTx). Consumed by the future escape-hatch refund flow.
rpc AllExpiredInbounds(QueryAllExpiredInboundsRequest) returns (QueryAllExpiredInboundsResponse) {
option (google.api.http).get = "/uexecutor/v1/expired_inbounds";
}
}

// ==========================
Expand Down Expand Up @@ -119,7 +128,18 @@ message QueryAllPendingInboundsRequest {
}

message QueryAllPendingInboundsResponse {
repeated string inbound_ids = 1;
// Full per-variant audit-trail entries.
repeated PendingInboundEntry entries = 1 [(gogoproto.nullable) = false];
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}

// Expired Inbounds
message QueryAllExpiredInboundsRequest {
cosmos.base.query.v1beta1.PageRequest pagination = 1;
}

message QueryAllExpiredInboundsResponse {
repeated ExpiredInboundEntry entries = 1 [(gogoproto.nullable) = false];
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}

Expand All @@ -141,11 +161,19 @@ message QueryAllUniversalTxResponse {
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}

// Pending outbound index entry
// Pending outbound index entry. Created by chain code at outbound creation
// (see create_outbound.go). Removed only when validators reach consensus
// on an OutboundObservation (see msg_vote_outbound.go). Ballot expiry does
// NOT remove the entry — operators investigate stuck outbounds via the
// per-variant audit trail (variants below) plus separate uvalidator ballot
// queries to see which ballots have terminated. See
// plan-pending-outbound-cleanup.md for design rationale.
message PendingOutboundEntry {
string outbound_id = 1;
string universal_tx_id = 2;
int64 created_at = 3;
// Per-variant audit trail, populated as votes arrive (RecordOutboundVote).
repeated OutboundObservationVariant variants = 4 [(gogoproto.nullable) = false];
}

message QueryGetPendingOutboundRequest {
Expand Down
Loading
Loading