Skip to content

UCIe · Module 12

Transaction Lifecycle

When a UCIe-carried transaction begins to exist, which state represents it at each layer, and the exact event that lets every layer forget it — a lifecycle FSM with legal-transition checking, one allocation per semantic transaction, retirement invariants, identity quarantine, timeout as ambiguity rather than evidence, first-failure preservation, and per-phase recovery policy.

Three chapters have followed things in motion. Chapter 12.1 followed a request outward, Chapter 12.2 followed the obligation back, and Chapter 12.3 followed the bytes through every reshaping stage.

Each of those chapters had to keep saying the same thing in different words: the transaction is still open. Open after the FDI handshake. Open after the replay entry retired. Open after the payload was entirely confirmed. Open until something specific happens.

This chapter is about that something. It asks the question the other three deferred: what is the transaction, as a piece of state, and what is the exact event that permits it to be forgotten?

The answer matters because getting it wrong is not a performance bug. A transaction forgotten early is a returning response with nothing to match; a transaction forgotten late is an identity that cannot be reused; and a transaction allocated twice is a system that thinks one operation is two.

1. The One-Sentence Model

A transaction is an obligation with a lifetime. It begins when a layer accepts responsibility, it ends when the semantic obligation is resolved, and physical transmission is one event inside that lifetime rather than either end of it.

Everything else in this chapter is that sentence made checkable.

2. Sourcing, and What Is Symbolic

3. Four Lifetimes, Named Precisely

The distinction the whole chapter rests on, and it is routinely collapsed into two.

BeginsEndsOwned by
Semantic transactiona layer accepts responsibility for the operationthe obligation is resolved — completed, or failed through a defined paththe Protocol Layer
Transport objectthe Adapter accepts the mapped representationtransport confirms deliverythe Adapter
Physical attempttransmission of that object startsthe bits are gonethe PHY
Responsethe far side produces itthe local consumer accepts itthe return path

Read the second row against the first. A transport object's lifetime is contained within the transaction's, and a replay is a second physical attempt at the same transport object — not a second transport object, and certainly not a second transaction. §9 is the bug of not believing that.

And read the fourth row against the first. A response arriving is not the transaction ending. Chapter 12.2 §17 established that retirement waits for consumer acceptance, and §17 here is why.

One semantic transaction may correspond to one transport object, several physical attempts, and one response. Collapsing any of those into the others produces a specific, named bug in this chapter.

4. The Lifecycle Phases

The phases a transaction passes through, as a teaching model.

PhaseWhat is trueWhich layer holds responsibility
DISPATCHEDaccepted locally; identity reserved; not yet queued for transportProtocol Layer
QUEUEDin the mapping or boundary queue; not yet framedAdapter (forward progress)
IN_TRANSPORTframed, retained for replay, possibly on the wireAdapter (recovery)
REMOTE_OWNEDthe far side has accepted it semanticallythe remote Protocol Layer
WAIT_RESPremote is executing; nothing is in flight locallythe remote side
RESP_PENDINGa response has arrived and been matched, but not consumedthe return path
COMPLETEthe consumer has the result; nothing is owednobody — retirement may proceed
FAILEDresolved through a defined error path, outcome possibly unknowndiagnostics

Two phases are the ones designs omit and then need.

RESP_PENDING exists because arrival is not consumption. Without it, a design has no state in which "the answer is here and the requester has not taken it" is representable — and that is precisely the window Chapter 12.2 §18's bug lives in.

REMOTE_OWNED exists because it is the phase in which a timeout is dangerous. A transaction that has been accepted remotely and whose response is late cannot be safely reissued (§20), and a design that cannot distinguish "not yet delivered" from "delivered, awaiting response" cannot make that judgement.

5. The Lifecycle State Machine

An illustrative transaction lifecycle state machine. FREE advances to DISPATCHED on semantic accept, then to QUEUED, then to IN_TRANSPORT when framed, then to REMOTE_OWNED when the far side accepts, then to WAIT_RESP, then to RESP_PENDING when a response is matched, then to COMPLETE when the consumer accepts, and back to FREE on retirement. A FAILED state is reachable from IN_TRANSPORT and from REMOTE_OWNED and is left only by an explicit policy decision.FREEDISPATCHEDQUEUEDIN_TRANSPORTREMOTE_OWNEDWAIT_RESPRESP_PENDINGCOMPLETEFAILEDsemantic acceptsemantic acceptqueuedqueuedframedframedremote acceptremote acceptexecutingexecutingresponse matchedresponse matchedconsumer acceptsconsumer acceptsretireretireunrecoverableunrecoverabletimeouttimeoutpolicy releasepolicyrelease
Figure 1 — an illustrative transaction-lifetime state machine, not a normative UCIe state machine. UCIe defines link states; a transaction's phases belong to the carried protocol and the implementation. Read the diagram for its shape: a single allocation edge into DISPATCHED, a single retirement edge out of COMPLETE, and a FAILED state that is reached from several phases and left only by an explicit policy rather than by a retry.

Three things the figure is drawn to show.

One edge into DISPATCHED and one out of COMPLETE. Allocation and retirement are single events, and §8 and §17 are those two edges made into properties.

FAILED is reached from two phases and they mean different things. From IN_TRANSPORT, the transaction may never have been delivered. From REMOTE_OWNED, it certainly was. Same state, completely different safe actions — §20.

And the only edge out of FAILED is a policy decision. Not a retry, not a timeout expiry. §21 is why.

6. The Transaction Descriptor

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE transaction state. NOT a UCIe or CXL structure. An enum rather
// than raw state bits, so an illegal encoding is not representable and the
// waveform is readable.
typedef enum logic [3:0] {
  TXN_FREE         = 4'd0,
  TXN_DISPATCHED   = 4'd1,
  TXN_QUEUED       = 4'd2,
  TXN_IN_TRANSPORT = 4'd3,
  TXN_REMOTE_OWNED = 4'd4,
  TXN_WAIT_RESP    = 4'd5,
  TXN_RESP_PENDING = 4'd6,
  TXN_COMPLETE     = 4'd7,
  TXN_FAILED       = 4'd8
} txn_state_t;
 
typedef struct packed {
  logic                valid;
  logic [TXN_ID_W-1:0] id;            // the identity the far side reflects
  logic [GEN_W-1:0]    gen;           // which attempt — Ch 12.2 §24
  txn_state_t          state;
  logic [META_W-1:0]   meta;          // address, direction, length
  logic                payload_done;  // Ch 12.3 — the bytes are all sent
  logic [AGE_W-1:0]    age;           // ticks since allocation, SATURATING
  logic [3:0]          retry_count;
  logic                fail_valid;    // §22 — first failure captured
  logic [3:0]          fail_cause;    // §22 — and never overwritten
  logic [MON_ID_W-1:0] mon_id;        // VERIFICATION ONLY
} txn_entry_t;
 
txn_entry_t txn_q [MAX_TXNS];

Architecture. One descriptor per in-flight transaction. Note what is not here: no transport sequence number, no replay pointer, no beat index. Those belong to other lifetimes (§3), and putting them here is how a design ends up retiring a transaction on a transport event.

Note payload_done explicitly. It is the one field that couples this table to Chapter 12.3, and it exists because a transaction can be in WAIT_RESP with its payload entirely sent or, for a write with a slow datapath, still in IN_TRANSPORT with bytes outstanding. Two independent facts, and a design with one "in flight" bit cannot express their combination.

State. MAX_TXNS entries with per-semantic-transaction lifetime. age, retry_count, fail_valid and fail_cause are diagnostic and deliberately outlive the functional need (§22).

Cycle behaviour. Allocated on semantic accept, transitioned by events, freed on retirement. §12's next-state function is the only writer of state.

Contract. The response matcher relies on valid and id; recovery relies on state; diagnostics rely on the last four fields surviving.

Failure. §9 for double allocation, §18 for early retirement, §22 for lost root cause.

DV. Cover every state; cover every field's boundary; and verify that fail_cause does not change once fail_valid is set.

7. Allocation — One Event, Defined Precisely

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE allocation. ONE event creates a transaction, and it is a
// SEMANTIC event — not a transport milestone.
wire alloc_fire = semantic_req_valid && semantic_req_ready;
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    for (int t = 0; t < MAX_TXNS; t++) begin
      txn_q[t].valid <= 1'b0;
      txn_q[t].state <= TXN_FREE;
    end
  end else if (alloc_fire) begin
    txn_q[alloc_idx].valid        <= 1'b1;
    txn_q[alloc_idx].id           <= alloc_id;
    txn_q[alloc_idx].gen          <= id_gen_q[alloc_id];
    txn_q[alloc_idx].state        <= TXN_DISPATCHED;
    txn_q[alloc_idx].meta         <= semantic_req_meta;
    txn_q[alloc_idx].payload_done <= 1'b0;
    // ENTRY actions, not residence actions — §13.
    txn_q[alloc_idx].age          <= '0;
    txn_q[alloc_idx].retry_count  <= '0;
    txn_q[alloc_idx].fail_valid   <= 1'b0;
    txn_q[alloc_idx].fail_cause   <= '0;
    txn_q[alloc_idx].mon_id       <= semantic_req_mon_id;
  end
end

Architecture. A single allocation point, at the semantic boundary. The CXL specification's phrasing is the authority worth quoting here: the request identity is pre-allocated for the duration of the transaction. Pre-allocated means before transmission; for the duration means until the obligation resolves.

State. One entry consumed per allocation.

Cycle behaviour. Every field is initialised on entry. Note that this is deliberately an entry action — the fields are written once, on the allocating cycle, and not refreshed while the transaction resides in DISPATCHED. §13 is why that distinction matters.

Contract. Downstream layers rely on the entry existing before anything is transmitted. Chapter 12.1 §10's admission gate includes table space for this reason.

Failure. §9.

DV. Allocate at every table index; allocate when the table is one slot from full; and — the important one — verify that no transport event anywhere in the design causes an allocation. That is a structural check as much as a simulation one.

8. Wrong RTL — Allocating Again on Replay

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the table is written whenever an object is handed to the Adapter,
// which happens again on every replay.
always_ff @(posedge clk) begin
  if (adapter_accept) begin
    txn_q[free_idx].valid <= 1'b1;         // a "new" transaction, every time
    txn_q[free_idx].id    <= object_id;
    txn_q[free_idx].state <= TXN_IN_TRANSPORT;
  end
end

Architecture. Allocation has been attached to a transport event rather than a semantic one. §3's second row says why that is a category error: a replay is another physical attempt at the same transport object, and the Adapter accepting an object for retransmission is not a new operation.

Cycle behaviour. First transmission allocates one entry. A CRC failure triggers a replay; the Adapter accepts the object again; a second entry is allocated for the same transaction.

Failure, and it cascades.

Two live entries now share one identity, which is Chapter 12.2 §9's forbidden state. The single returning response matches both — or matches whichever the priority encoder picks — so one entry retires and the other never does.

The orphaned entry leaks. It sits valid forever, so its identity is never released. Under sustained retries the table fills with orphans until allocation fails, and the symptom is a system that stops accepting transactions after a period of link stress. The cause is minutes in the past and the symptom is resource exhaustion, which sends the investigation to buffer sizing.

And the accounting is wrong in a way that hides it. allocated has counted two, completed counts one, so the conservation equation of §26 shows one transaction permanently outstanding — which is the correct alarm, and it fires at end of test rather than at the moment of the bug.

Why it survives review. adapter_accept looks like a reasonable allocation trigger, and in a run with no errors it fires exactly once per transaction. It requires a retry to expose, so a regression that does not inject CRC failures never sees it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — exactly one allocation per semantic transaction. Uses the
// verification-only tag, because the protocol identity is reused and cannot
// distinguish "this transaction again" from "a later one with the same id".
property p_one_allocation_per_transaction;
  @(posedge clk) disable iff (!rst_n)
    alloc_fire |-> !allocated_mon[semantic_req_mon_id];
endproperty
a_one_allocation_per_transaction:
  assert property (p_one_allocation_per_transaction);
 
// Illustrative — no transport event allocates. Written over the events rather
// than the state, so it fires even if the resulting state looks plausible.
property p_transport_does_not_allocate;
  @(posedge clk) disable iff (!rst_n)
    (adapter_accept || transport_retry_event) |=> (txn_alloc_count == $past(txn_alloc_count));
endproperty
a_transport_does_not_allocate: assert property (p_transport_does_not_allocate);

9. The Lifecycle Next-State Function

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE lifecycle next-state logic for one transaction entry.
// ONE always_comb, ONE priority order, ONE writer of `state`. The priority is
// THIS MODEL'S POLICY and is not a UCIe or CXL requirement.
function automatic txn_state_t txn_next_state(
    input txn_state_t     cur,
    input txn_events_t    ev        // packed struct of this cycle's events
);
  txn_next_state = cur;                        // default: hold
 
  unique case (1'b1)
    // ---- 1. A defined failure wins over everything below it. Nothing further
    //         down can be trusted once the transaction is being torn down.
    ev.fail_event:                txn_next_state = TXN_FAILED;
 
    // ---- 2. Forward progress, in phase order.
    ev.queued        && (cur == TXN_DISPATCHED):    txn_next_state = TXN_QUEUED;
    ev.framed        && (cur == TXN_QUEUED):        txn_next_state = TXN_IN_TRANSPORT;
    ev.remote_accept && (cur == TXN_IN_TRANSPORT):  txn_next_state = TXN_REMOTE_OWNED;
    ev.exec_started  && (cur == TXN_REMOTE_OWNED):  txn_next_state = TXN_WAIT_RESP;
    ev.resp_matched  && (cur == TXN_WAIT_RESP):     txn_next_state = TXN_RESP_PENDING;
    ev.consumer_ack  && (cur == TXN_RESP_PENDING):  txn_next_state = TXN_COMPLETE;
    ev.retire        && (cur == TXN_COMPLETE):      txn_next_state = TXN_FREE;
    ev.policy_release&& (cur == TXN_FAILED):        txn_next_state = TXN_FREE;
 
    default: ;                                 // hold
  endcase
endfunction

Architecture. Each forward transition is guarded by both the event and the current state. That double guard is what makes the machine reject an event arriving in the wrong phase instead of acting on it — a response matched while the transaction is still QUEUED is not a legal situation, and the machine holds rather than jumping.

State. None — a pure function. The registered state field is written from exactly one place, which calls this.

Cycle behaviour. unique case (1'b1) makes the priority explicit and turns an unintended overlap into a simulation error rather than a silent precedence. The failure event is first because a transaction being torn down must not simultaneously make forward progress.

Contract. §11's legality function and this function must agree; §11 asserts that they do.

Failure. Guarding a transition on the event alone — ev.resp_matched: state = TXN_RESP_PENDING with no state check — lets a transaction skip phases (§10). Putting the failure event last lets a forward transition win over a teardown, so a failing transaction advances as though healthy.

DV. Drive every event in every state — the full cross — and verify that only the eight legal combinations move the state. That cross is the coverage model, and the illegal combinations are the interesting part: they should all be reached and all be ignored.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE transition legality. A single source of truth, used by the
// assertion so the two cannot drift. Kept local to this teaching model — not
// a repository utility.
function automatic logic legal_txn_transition(
    input txn_state_t from,
    input txn_state_t to
);
  if (from == to) return 1'b1;                    // holding is always legal
  unique case (from)
    TXN_FREE         : return (to == TXN_DISPATCHED);
    TXN_DISPATCHED   : return (to == TXN_QUEUED)       || (to == TXN_FAILED);
    TXN_QUEUED       : return (to == TXN_IN_TRANSPORT) || (to == TXN_FAILED);
    TXN_IN_TRANSPORT : return (to == TXN_REMOTE_OWNED) || (to == TXN_FAILED);
    TXN_REMOTE_OWNED : return (to == TXN_WAIT_RESP)    || (to == TXN_FAILED);
    TXN_WAIT_RESP    : return (to == TXN_RESP_PENDING) || (to == TXN_FAILED);
    TXN_RESP_PENDING : return (to == TXN_COMPLETE)     || (to == TXN_FAILED);
    TXN_COMPLETE     : return (to == TXN_FREE);
    TXN_FAILED       : return (to == TXN_FREE);
    default          : return 1'b0;
  endcase
endfunction
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — every observed transition is legal. One property covering the
// entire machine, which is why the legality function is worth writing.
property p_legal_transition;
  @(posedge clk) disable iff (!rst_n)
    txn_q[t].valid |=> legal_txn_transition($past(txn_q[t].state), txn_q[t].state);
endproperty
a_legal_transition: assert property (p_legal_transition);
 
// Illustrative — the specific skip that matters most: COMPLETE without
// evidence of remote ownership and a response. Written separately because it
// is the transition a "shortcut for performance" edit introduces.
property p_no_complete_without_evidence;
  @(posedge clk) disable iff (!rst_n)
    (txn_q[t].state == TXN_COMPLETE)
      |-> (remote_accept_seen_q[t] && response_seen_q[t]);
endproperty
a_no_complete_without_evidence:
  assert property (p_no_complete_without_evidence);

Why the legality function earns its place. Without it, checking the machine means writing one property per legal edge — nine states means dozens of properties, they drift from the design, and a new state added later is unprotected. With it, one property covers the whole machine and adding a state means editing one function.

And why p_no_complete_without_evidence is separate. legal_txn_transition forbids DISPATCHED → COMPLETE structurally. But a design can reach COMPLETE through the legal chain while the evidence was never observed — if, say, ev.remote_accept were driven by a transport event rather than a semantic one. The transition is legal and the conclusion is false, so the property is written over the accumulated evidence rather than over the edge.

11. Entry Actions Versus Residence Actions

A small discipline with a real bug behind it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — fields refreshed while the transaction merely resides in a state.
always_ff @(posedge clk) begin
  if (txn_q[t].state == TXN_DISPATCHED) begin
    txn_q[t].age         <= '0;        // reset every cycle it sits here
    txn_q[t].retry_count <= '0;
  end
end

Failure. A transaction stalled in DISPATCHED — waiting for queue space — has its age reset on every cycle it waits. So its age is permanently zero, and it is the one transaction whose age matters. A timeout policy reading age never fires for a transaction that is stuck in exactly the phase where being stuck is the symptom.

And retry_count cleared on residence loses the retry history that §22's first-failure logic depends on.

The right shape is the one §7 uses: these are entry actions, written on the allocating or transitioning cycle only.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — entry action: written on the transition, not during residence.
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    txn_q[t].age <= '0;
  end else if (txn_state_changing[t]) begin
    txn_q[t].age <= '0;                       // per-phase age, on entry only
  end else if (txn_q[t].valid && (txn_q[t].age != {AGE_W{1'b1}})) begin
    txn_q[t].age <= txn_q[t].age + AGE_W'(1); // SATURATES — Ch 12.1 §27
  end
end

Note the design choice made explicit. Resetting age on every transition gives a per-phase age, which answers "how long has it been stuck here". Resetting only at allocation gives a total age, which answers "how long has this transaction existed". Both are useful and they are different; a design should know which one it has. This model takes per-phase, because §27's taxonomy is organised by which phase a transaction is stuck in.

12. The Identity Allocator

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE identity allocation. Chapter 12.2 §9 built this for the
// response path; here it is the lifecycle's view of the same resource. Two
// bitmaps, because "not in use" and "safe to use" are different questions.
logic [MAX_IDS-1:0] id_in_use_q;
logic [MAX_IDS-1:0] id_quarantined_q;   // retired from a FAILED transaction
logic [GEN_W-1:0]   id_gen_q [MAX_IDS];
 
wire [MAX_IDS-1:0] id_allocatable = ~id_in_use_q & ~id_quarantined_q;
wire               id_available   = (id_allocatable != '0);
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    id_in_use_q      <= '0;
    id_quarantined_q <= '0;
    for (int i = 0; i < MAX_IDS; i++) id_gen_q[i] <= '0;
  end else begin
    if (alloc_fire) begin
      id_in_use_q[alloc_id] <= 1'b1;
      // The generation increments at ALLOCATION, so the value the request
      // carries is the value a legitimate response reflects (Ch 12.2 §24).
      id_gen_q[alloc_id]    <= id_gen_q[alloc_id] + GEN_W'(1);
    end
    // A NORMAL retirement releases the identity immediately.
    if (retire_normal) id_in_use_q[retire_id] <= 1'b0;
    // A FAILED transaction's identity is quarantined instead (§21).
    if (retire_failed) begin
      id_in_use_q[fail_id]      <= 1'b0;
      id_quarantined_q[fail_id] <= 1'b1;
    end
    if (quarantine_release) id_quarantined_q[release_id] <= 1'b0;
  end
end

Architecture. The identity space is a resource with a different lifetime from the table slot. A design can have a free table entry and no free identity, or the reverse, and conflating them is why §19's allocation check has two terms.

State. Two bits and a generation per identity, per-identity lifetime. The generation must survive retirement — it is the only field in this chapter whose value is meaningless except compared with a previous value of itself.

Cycle behaviour. Allocation consumes and bumps. Normal retirement releases. Failed retirement quarantines, which is the whole point of having two bitmaps.

Contract. Chapter 12.2's matcher relies on at most one live entry per identity and on the generation distinguishing attempts.

Failure. §13.

DV. Exhaust the identity space; retire and immediately reallocate; fail a transaction and attempt to reallocate its identity (must be refused); and wrap the generation counter.

13. Wrong RTL — Releasing an Identity on Transmission

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the identity freed when the request leaves.
always_ff @(posedge clk) begin
  if (request_transmitted) id_in_use_q[tx_id] <= 1'b0;
end

Failure. Chapter 12.2 §8 and §27 develop this in full and the lifecycle framing adds one thing: the identity's lifetime is the transaction's, and the transaction is in IN_TRANSPORT at this moment — three phases short of retirement.

So the sequence is: identity 5 released while its transaction is still in flight; identity 5 reallocated to a new transaction; the original's response arrives carrying identity 5 and matches the new transaction; the new requester receives the old request's data. One-hot match, balanced counts, clean CRC, wrong data.

The lifecycle view makes the fix obvious in a way the response-path view does not. The release condition should not be a transport event at all — it should be a state transition, and specifically the transition out of COMPLETE or out of FAILED. Writing it that way makes the bug unrepresentable:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — identity release is a consequence of retirement, and
// retirement is a state transition. No transport event appears.
assign retire_normal = (txn_q[t].state == TXN_COMPLETE) && ev.retire;
assign retire_failed = (txn_q[t].state == TXN_FAILED)   && ev.policy_release;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — an identity is live for exactly as long as its transaction.
property p_id_live_iff_transaction_live;
  @(posedge clk) disable iff (!rst_n)
    id_in_use_q[i] == transaction_exists_with_id(i);
endproperty
a_id_live_iff_transaction_live: assert property (p_id_live_iff_transaction_live);
 
// Illustrative — never allocate an identity that is in use or quarantined.
property p_alloc_only_free_id;
  @(posedge clk) disable iff (!rst_n)
    alloc_fire |-> (!id_in_use_q[alloc_id] && !id_quarantined_q[alloc_id]);
endproperty
a_alloc_only_free_id: assert property (p_alloc_only_free_id);

14. Retirement — the Event, Defined

Retirement means: all state required for future semantic correctness may be forgotten. That is a strong claim and it is worth testing candidate events against it.

CandidateMay state be forgotten?Why not
request transmittednothe response has nowhere to match (12.2 §8)
replay entry retirednotransport is done; the obligation is not (12.1 §20, cycle 20)
remote acceptednothe far side owes a response; nothing has come back
payload fully sentno12.3 §4 — data done, transaction open
response arrived locallynothe consumer has not taken it (12.2 §18)
response matchednomatched is not delivered
response accepted by the consumeryesthe requester has the result; nothing is owed
failure resolved by policyyesthe outcome is defined and reported, even if unknown

Seven wrong answers and two right ones, and the seven are all events that feel like completion. That is why this table is worth more than a rule: the wrong answers are individually plausible and each has shipped.

Note the second right answer. A failed transaction can also be retired — but only after its outcome has been reported and its identity quarantined (§21). Retiring a failure silently is a silent discard.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — nothing retires before completion or a resolved failure.
property p_no_early_retirement;
  @(posedge clk) disable iff (!rst_n)
    (txn_q[t].valid && $fell(txn_q[t].valid))
      |-> ($past(txn_q[t].state) inside {TXN_COMPLETE, TXN_FAILED});
endproperty
a_no_early_retirement: assert property (p_no_early_retirement);
 
// Illustrative — and after retirement the entry really is free.
property p_retired_entry_not_live;
  @(posedge clk) disable iff (!rst_n)
    (txn_q[t].state == TXN_FREE) |-> !txn_q[t].valid;
endproperty
a_retired_entry_not_live: assert property (p_retired_entry_not_live);

On writing p_no_early_retirement over the falling edge of valid rather than over each candidate event. That framing catches every path that clears the entry — a transport event, a stray reset, a debug backdoor, a future edit — without enumerating them. It is the same technique Chapter 12.2 §18 used, and it is the most robust shape for a "nothing may do X" property.

15. Wrong RTL — Retiring When the Response Reaches the Chip

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — retirement on local arrival, before the consumer has the data.
always_ff @(posedge clk) begin
  if (response_arrived && response_matched) begin
    txn_q[rsp_idx].valid <= 1'b0;
    txn_q[rsp_idx].state <= TXN_FREE;
  end
end

Architecture. It skips RESP_PENDING — the state §4 said designs omit and then need. With no such state, "the answer is here and unconsumed" is unrepresentable, so the design has to pick either arrival or consumption, and arrival is the one that looks like completion.

Failure, three ways, all of them data loss.

The consumer stalls and the identity is reallocated. The queued response is then delivered against a new transaction's entry — §13's alias, generated locally rather than arriving from the link.

The response queue overflows. The entry is already gone, so nothing records that anything was owed. The requester waits forever, and the only evidence is §26's conservation imbalance at end of test.

A reset clears the queue. Response gone, entry gone, no record that an obligation existed.

Why it survives review. response_arrived && response_matched reads exactly like completion, and with a consumer that is always ready — which is what a simple testbench provides — arrival and acceptance are the same cycle.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — the entry survives while a matched response waits.
property p_entry_retained_while_response_pending;
  @(posedge clk) disable iff (!rst_n)
    (txn_q[t].state == TXN_RESP_PENDING) && !ev.consumer_ack
      |=> (txn_q[t].valid && (txn_q[t].state == TXN_RESP_PENDING));
endproperty
a_entry_retained_while_response_pending:
  assert property (p_entry_retained_while_response_pending);

16. Timeout Is Ambiguity, Not Evidence

Chapter 12.1 §25 established the principle. The lifecycle adds the part that makes it actionable: which phase the transaction was in when the timeout fired determines what is safe.

Phase at timeoutWhat may have happenedSafe action
DISPATCHEDnever left the diereissue is safe — nothing was observed remotely
QUEUEDnever framedreissue is safe
IN_TRANSPORTunknown — may or may not have arrivedcannot reissue safely; escalate, or reissue only if the operation is idempotent
REMOTE_OWNEDcertainly executed; the response is lost or latemust not reissue a non-idempotent operation
WAIT_RESPcertainly executedmust not reissue
RESP_PENDINGcompleted remotely; the local consumer is stallednot a remote problem at all — this is local backpressure

Two rows are the whole value of having a lifecycle state.

The first two rows make reissue safe, and a design without phase information cannot know that — so it must treat every timeout as the worst case and escalate transactions that were never even transmitted.

And the last row is not a timeout condition at all. A transaction in RESP_PENDING for a long time means the local consumer is not accepting. Escalating that as a remote failure is a misdiagnosis that a phase-aware design cannot make.

A timeout without a phase is an alarm with no information. A timeout with a phase is a diagnosis.

17. Wrong RTL — Blind Reissue on Timeout

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the phase is ignored, so a transaction the far side already
// executed is issued a second time.
always_ff @(posedge clk) begin
  if (txn_timeout[t]) begin
    reissue_request[t] <= 1'b1;
    txn_q[t].state     <= TXN_DISPATCHED;    // "start again"
  end
end

Failure. For a transaction in REMOTE_OWNED or later, the operation is executed twice. A non-idempotent write — an increment, a doorbell, a queue push — corrupts state permanently with no error anywhere. And the reissue enters as a new semantic operation, so the duplicate-suppression that makes transport replay safe (Chapter 11.4 §17) does not apply: that mechanism suppresses duplicate transport objects, and this is a second semantic transaction.

And the state transition is itself illegal. REMOTE_OWNED → DISPATCHED is not in §10's legality function, so p_legal_transition fires — which is the value of having written that property: a recovery shortcut that violates the lifecycle is caught by a property about the lifecycle, not by a property about reissue.

The defensible handler:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — a timeout moves to FAILED and records why. Reissue is a
// SEPARATE, narrower decision made only where the phase makes it safe.
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    // ...
  end else if (txn_timeout[t]) begin
    if (txn_q[t].state inside {TXN_DISPATCHED, TXN_QUEUED}) begin
      // Never observed remotely: a reissue is safe and the transaction stays
      // the same semantic operation.
      txn_q[t].state <= TXN_DISPATCHED;
      txn_q[t].retry_count <= txn_q[t].retry_count + 4'd1;
    end else begin
      // Possibly or certainly executed. The outcome is UNKNOWN, which is a
      // different and more honest state than "not delivered".
      txn_q[t].state <= TXN_FAILED;
      if (!txn_q[t].fail_valid) begin        // §22 — first cause only
        txn_q[t].fail_valid <= 1'b1;
        txn_q[t].fail_cause <= FAIL_TIMEOUT_AMBIGUOUS;
      end
    end
  end
end

18. Failed Is a State With a Lifetime, Not a Cleanup

A transaction that fails must not be swept away, and the reasons are diagnostic rather than functional.

What must be retained while a transaction sits in FAILED:

  • the original identity — so a late response can be recognised as belonging to it rather than aliased onto a new transaction;
  • the first failure cause — §22;
  • the phase it failed from — because §16's table makes that the difference between a benign and a dangerous failure;
  • the age and retry count — the only record of how long and how hard the system tried.

And the identity must be quarantined rather than released (§12), because a late response for a failed transaction will arrive if the far side executed it.

The exit from FAILED is a policy decision, and this chapter deliberately does not define the policy: it depends on the carried protocol's error model and on what software expects. What it does define is the shape — the exit must be explicit, and the state must be reportable before it happens.

A failure that is cleaned up without being reported is indistinguishable from a transaction that never existed.

19. First-Failure Preservation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE first-error capture. The `if (!valid)` guard is the entire
// mechanism, and omitting it is §20.
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    txn_q[t].fail_valid <= 1'b0;
    txn_q[t].fail_cause <= '0;
  end else if (alloc_fire && (alloc_idx == t)) begin
    txn_q[t].fail_valid <= 1'b0;              // entry action — fresh per txn
    txn_q[t].fail_cause <= '0;
  end else if (!txn_q[t].fail_valid && fail_event[t]) begin
    txn_q[t].fail_valid <= 1'b1;
    txn_q[t].fail_cause <= fail_cause_in[t];  // written ONCE, never updated
  end
end

Architecture. One write-once field per transaction. It exists because failures cascade, and the last failure in a cascade is almost never the useful one.

State. Per-transaction, diagnostic lifetime — it survives the transaction's functional resolution and is cleared only at allocation of a new transaction in that slot.

Cycle behaviour. Cleared on entry (§11's discipline). Written on the first failure and then held.

Contract. Debug reads it and expects the root cause. A timeout policy may read it to decide escalation.

Failure. §20.

DV. Inject two failures on one transaction and verify the first is retained; verify the field is cleared at allocation and not at retirement, because a stale cause from the slot's previous occupant is worse than none.

20. Wrong RTL — Last Failure Overwrites the Root Cause

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — no guard, so every failure overwrites the record.
always_ff @(posedge clk) begin
  if (fail_event[t]) txn_q[t].fail_cause <= fail_cause_in[t];
end

Failure, and it is a debugging failure rather than a functional one — which is why it survives indefinitely.

The realistic cascade: a transport error occurs first and the transaction moves toward failure. The cleanup path then stalls — perhaps because the link is still recovering — and a timeout fires. The record now says TIMEOUT.

So the diagnostic points at the symptom and the cause is gone. An engineer reads "timeout", investigates timeouts, adjusts thresholds, and the transport error that started it is never examined. The system appears to have a timing problem and has an integrity problem.

And it compounds across transactions. In a link event that fails many transactions at once, all of them report whatever failed last — usually the same generic cleanup error — so the distribution of causes is destroyed too, which is the information that would have identified which mechanism broke.

The fix is one guard, and it is the cheapest diagnostic investment in this chapter.

21. The Transaction Table's Accounting

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE occupancy maintenance. One writer, four enumerated outcomes.
logic [TXN_CNT_W-1:0] txn_count_q;
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    txn_count_q <= '0;
  end else begin
    unique case ({alloc_fire, retire_fire})
      2'b10: txn_count_q <= txn_count_q + TXN_CNT_W'(1);
      2'b01: txn_count_q <= txn_count_q - TXN_CNT_W'(1);
      2'b11: txn_count_q <= txn_count_q;      // one in, one out — UNCHANGED
      2'b00: txn_count_q <= txn_count_q;
    endcase
  end
end
 
assign txn_space = (txn_count_q != MAX_TXNS);

Cycle behaviour. The 2'b11 row is the steady state at the outstanding limit, not a corner case: retirement is what creates the space allocation consumes, so a saturated system hits it continuously. Chapter 12.2 §19 made this point for the outstanding table and it applies identically here.

Failure. Two independent updates drift the count downward, so txn_space is asserted when the table is genuinely full and a transaction is allocated into an occupied entry — overwriting a live transaction, whose response then matches nothing and whose requester waits forever.

The redundant check that costs one property and catches every drift:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — the count must equal the number of valid entries. Deliberately
// redundant: the two must never disagree, and $countones needs no model.
property p_count_matches_valid_entries;
  @(posedge clk) disable iff (!rst_n)
    txn_count_q == $countones(txn_valid_vec);
endproperty
a_count_matches_valid_entries: assert property (p_count_matches_valid_entries);

On practicality. $countones over a wide vector is a simulation and formal construct, not something to synthesise. That is fine — this is a verification property, and its value is that it needs no reference model at all, so it can be written on day one and left in for the project's life.

22. Independent Progress, Shared Resources

Transactions have independent lifecycles and share transport, and both halves matter.

Independent: transaction A in WAIT_RESP does not prevent transaction B from reaching COMPLETE. There is no global serialisation, and a design that imposes one has converted a protocol that permits concurrency into one that does not.

Shared: A and B compete for queue space, replay entries, credits, and the identity space. So B's progress can be blocked by A's resource consumption even though their lifecycles are independent.

The distinction is what makes §27's taxonomy work. A transaction stuck in QUEUED is a resource problem — something else is holding what it needs. A transaction stuck in REMOTE_OWNED is a remote problem. Same symptom from the outside, opposite investigations, and the phase is what separates them.

And the head-of-line case is worth naming. If the transport is strictly ordered, A's stall does delay B's transmission — but that delays B's transition from QUEUED to IN_TRANSPORT, not its lifecycle's validity. Chapter 12.2 §29 covers the return-path version of the same trade.

23. Conservation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
allocated  =  retired  +  live  +  failed_not_yet_retired

Checked continuously, and at end of test with live required to be zero under progress assumptions.

The clause that makes it correct, and it is the one §8's bug violates:

A physical replay does not increase allocated. Retransmission is another attempt at a transport object, and transport objects are not counted here. This equation counts semantic transactions only.

Three checks it supports:

The equation itself. Catches §21's drift and §8's double allocation — the latter shows as allocated exceeding the sum, because one of the two entries never retires.

allocated per monitor tag equals one. The per-transaction form of §8's property, checked in the model rather than the design.

And every allocated transaction reaches COMPLETE or FAILED. An end-of-test check, and it is the one that catches a transaction stuck forever in an intermediate phase — which no in-run property fires on, because sitting in a state is legal.

24. Recovery, Phase by Phase

A UCIe link enters recovery with transactions spread across the lifecycle. They cannot be treated identically, and the phase is the discriminator.

Phase at recoveryWhat is trueSafe action
DISPATCHEDnothing has leftretain — nothing to undo
QUEUEDnothing has leftretain; the queue entry is not link state
IN_TRANSPORTthe transport object may be mid-flighttransport policy — replay state is re-baselined with the peer (9.4); the transaction is retained
REMOTE_OWNEDthe far side has itcannot reissue; retain and wait, or escalate
WAIT_RESPthe far side is executingretain matching state — the response may still come
RESP_PENDINGthe response is already localunaffected by the link — this transaction needs nothing from recovery
FAILEDalready resolvedretain diagnostics and the quarantine

Three observations that generalise.

Transport state re-baselines; transaction state does not. Chapter 11.5 §17 made this argument across three planes; here it is the same rule expressed per phase. Credits and replay entries are link-epoch state and are correctly re-established. Transaction entries are not.

RESP_PENDING is the phase that proves the rule. Its response is already in a local queue. A recovery that swept the transaction table would destroy a transaction whose answer had already arrived — the clearest possible demonstration that a link event and a transaction lifetime are different scopes.

And REMOTE_OWNED is the phase with no local answer, which is Chapter 11.5 §17's distributed-state problem in its simplest form.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative — a link recovery does not disturb the transaction table.
// The property is scoped to the table deliberately: it says nothing about
// credits or replay state, which SHOULD be re-baselined.
property p_recovery_preserves_transaction_table;
  @(posedge clk) disable iff (!rst_n)
    ucie_recovery_event |=> ($stable(txn_valid_vec) && $stable(txn_count_q));
endproperty
a_recovery_preserves_transaction_table:
  assert property (p_recovery_preserves_transaction_table);

25. Reset Is Three Different Facts

A distinction Chapter 5.5 §7 established for layers, applied here to transactions.

Reset kindScopeWhat it may clear
Local logic resetone block's internal statepipeline registers, converter state, FIFO pointers
Link resetthe link epochcredits, replay state, negotiated parameters, the format epoch
Semantic abortone transaction, or alltransaction entries — and only with an explicit, reported abort

The failure is using the first to accomplish the third. A local reset asserted to recover a stuck datapath, which happens to also clear the transaction table, has silently aborted every in-flight transaction. Software is still waiting for all of them; nothing was reported; the link looks healthy.

And it is a plausible design. A single soft_reset that clears "everything in this block" is simpler than three scoped resets, and it works in bring-up when nothing is in flight. It fails the first time it is used as a runtime recovery mechanism, which is exactly when it is most needed.

The scope of a reset must match the lifetime of the state it clears. Three lifetimes need three resets, and a transaction may only be destroyed by an event that reports the destruction.

26. The Lifecycle Sequence

A requester allocates a transaction entry and hands the request to the adapter, which frames it and retains a replay copy before transmitting. The remote side accepts the request semantically and executes it, then returns a response. The adapter's replay entry retires on confirmation. The response is matched against the still-live transaction entry, the consumer accepts it, and only then is the transaction retired and its identity released.One transaction, one lifetime — conceptualRequesterTxn tableAdapterRemote sideallocate: DISPATCHEDqueued, then framedtransmit; replayretainedremote accept:REMOTE_OWNEDconfirmed: replayretiresresponse matched:RESP_PENDINGconsumer accepts:COMPLETEretire; releaseidentity
Figure 2 — one transaction's lifecycle as an exchange, annotated with the state it occupies rather than only with the messages. The requester's entry is allocated before anything is transmitted and released only after the consumer has the result. Note that the replay entry retires several steps before the transaction does, and that the remote side's ownership begins and ends inside the transaction's lifetime. Labels are conceptual, not UCIe signalling.

The fifth message is the one to notice. The replay entry retires before the response has even been matched — which is Chapter 12.1 §20's cycle 17 and 20 relationship drawn as an exchange. Transport finishing early is normal; it is not the transaction finishing.

27. Lifecycle Trace — the Successful Path

Eighteen illustrative cycles for one read transaction.

CycStateTableReplayRemoteResponseAction
1request presented
2DISPATCHED1 entryallocated; id 7 claimed, gen 3
3QUEUED1boundary queue took it
4IN_TRANSPORT1objectframed; replay retained
6IN_TRANSPORT1objectin flighton the lanes
8REMOTE_OWNED1objectaccepted oncefar side owns it
9WAIT_RESP1objectexecutingreading memory
11WAIT_RESP1executingconfirmed → replay retires
14WAIT_RESP1donein flightresponse returning
15RESP_PENDING1matched, id 7 gen 3entry still live
16RESP_PENDING1heldconsumer not ready
17COMPLETE1consumer accepted
18FREE0retired; id 7 released

Five things to read off it.

Cycle 2 is the only allocation. Nothing later allocates, including cycle 4's framing — which is §8's bug if it did.

Cycle 11: the replay entry retires with seven cycles of transaction lifetime still to run. Transport is finished. The obligation is not.

Cycle 15 is not completion. The response is matched and the entry is still live — RESP_PENDING exists precisely for this row.

Cycle 16 is the row §15's bug destroys. The consumer stalls for one cycle; a design retiring at cycle 15 has released identity 7 with the response still queued.

And cycle 18 releases the identity as a consequence of the state transition, not of any transport event — §13's fix expressed as a trace row.

28. Lifecycle Trace — the Ambiguous Timeout

The second trace, and the one that connects this chapter to Chapter 12.2.

CycStateTableIdentity 5RemoteAction
1DISPATCHED1in use, gen 1allocated
4IN_TRANSPORT1in useframed and sent
8REMOTE_OWNED1in useaccepted and executedfar side did the work
9WAIT_RESP1in useresponse produced
12WAIT_RESP1in useresponse delayedqueued behind a burst
40WAIT_RESP1in usestill delayedage climbing
50FAILED1quarantinedtimeout; phase was WAIT_RESP → ambiguous
51FAILED1quarantinedreported upward; cause recorded
55FAILED1quarantinednew transaction needs an id → gets id 9
70FAILED1quarantinedlate response arrives, id 5 gen 1
71FAILED1quarantinedmatches nothing live; logged as late
90FREE0releasedpolicy releases the quarantine

Four things this trace establishes.

Cycle 50's decision depended on the phase. WAIT_RESP means the far side certainly executed, so the transaction moves to FAILED with an ambiguous outcome rather than being reissued. A phase-blind design would have reissued and executed the operation twice.

The identity is quarantined, not released. Which is why cycle 55's new transaction gets identity 9 — and why cycle 70's late response finds nothing to alias onto. Without the quarantine, cycle 55 takes identity 5 and cycle 70 delivers the old response to the new transaction (Chapter 12.2 §25).

Cycle 71 produces a log line rather than a symptom. The late response proves the request was delivered, which converts an unexplained timeout into a latency problem — the single most valuable diagnostic in the sequence, and dropping it silently discards it.

And cycle 90 is a policy event. Not a timer expiring into a functional action; an explicit decision that the ambiguity has been resolved and the identity is safe.

29. The Transaction Scoreboard

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
JOIN KEY — the verification-only monitor ID. Required because the protocol
identity is reused across attempts and generations.
 
per transaction (by mon_id):
  allocated_at       : cycle, and it must happen exactly ONCE
  phase_history[]    : (state, entered_cycle) for every phase occupied
  remote_seen        : bool
  response_seen      : bool
  completed          : bool
  failed             : bool, with first_cause
  retired_at         : cycle, exactly once
  id_used, gen_used
 
aggregate:
  allocated, retired, live, failed_not_retired

The five checks, and the bug each catches.

allocated is exactly one per monitor tag. Catches §8 — allocation on a transport event shows as two allocations for one tag.

phase_history contains only legal transitions. The model's independent copy of §10's legality function. It catches a design whose transition guard was written on the event alone, because the resulting history contains a skip.

retired_at is set exactly once, and only after completed or failed. Catches §15 and §14's seven wrong retirement points.

Every allocated transaction reaches completed or failed by end of test. Catches a transaction parked forever in an intermediate phase, which no in-run property fires on because residence is legal.

And first_cause never changes once set. Catches §20.

On what the model must not do. It must not read the design's txn_count_q to derive live — that counter is §21's suspect. Count from observed allocation and retirement events at the interfaces.

30. Coverage

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Illustrative lifecycle coverage. Not UCIe-defined. Every bin exists to reach
// a specific failure in this chapter.
covergroup cg_txn_lifecycle @(posedge clk iff txn_event);
 
  cp_state     : coverpoint txn_q[t].state;        // every phase, §4
  cp_trans     : coverpoint txn_transition_taken;  // every legal edge, §10
  cp_occ       : coverpoint txn_count_q {
    bins empty = {0}; bins mid = {[1:$-1]}; bins full = {MAX_TXNS};
  }
  cp_simul     : coverpoint alloc_retire_same_cycle;      // §21's 2'b11
  cp_retry     : coverpoint txn_retry_count { bins none = {0}; bins one = {1}; bins many = {[2:$]}; }
  cp_to_phase  : coverpoint timeout_phase;         // §16 — WHICH phase timed out
  cp_recovery  : coverpoint recovery_phase;        // §24 — phase at recovery
  cp_reuse     : coverpoint id_reused_after_retire;
  cp_quar      : coverpoint id_reused_after_quarantine;   // must be UNREACHED
  cp_late      : coverpoint late_response_seen;     // §28 — must be non-zero
  cp_fail2     : coverpoint second_failure_on_txn;  // §19/§20
  cp_live      : coverpoint concurrent_live_txns { bins one = {1}; bins few = {[2:3]}; bins many = {[4:$]}; }
 
  // A timeout in every phase — the cross §16's table needs, and the only way
  // to prove the phase-dependent policy exists.
  x_timeout_phase : cross cp_to_phase, cp_retry;
  // A recovery with transactions in every phase — §24's table, executed.
  x_recovery_phase: cross cp_recovery, cp_live;
  // Simultaneous allocate/retire at the outstanding limit — §21's steady state.
  x_simul_full    : cross cp_simul, cp_occ;
  // A second failure on a transaction already failed — §20's overwrite.
  x_fail_cascade  : cross cp_fail2, cp_to_phase;
 
endgroup

Three notes on the bins.

cp_quar must stay at zero. Reusing a quarantined identity is forbidden by §12, so this is a bin whose value is that it never fills — and writing it down converts an assumption into a checked one.

cp_late must be non-zero. A regression that never observes a late response has not exercised the quarantine or the generation check, and "no aliasing failures" is then indistinguishable from "the defence was never tested".

And x_timeout_phase is the cross that proves §16 exists. A timeout in DISPATCHED and a timeout in REMOTE_OWNED must produce different actions. A suite that only ever times out in one phase cannot tell whether the policy is phase-aware.

31. Debug Taxonomy

SymptomPhaseWhat it means
Stuck in DISPATCHEDlocaladmission or resource — table space, identity space, or queue space. Nothing has left.
Stuck in QUEUEDlocalthe boundary or mapping queue is not draining — another transaction's resources (12.1 §28)
Stuck in IN_TRANSPORTtransportlink or replay — Chapter 9.4. Check retry counts.
Stuck in REMOTE_OWNEDremotethe far side has it and is not responding — remote execution or the return path
Stuck in RESP_PENDINGlocal consumerthe answer is here and nobody is taking it. Not a link problem at all.
Duplicate transaction for one operationallocation§8 — allocation on a transport event; look for a retry
Response matches nothingidentity lifetime§13 — identity released early, or a late response after a failure
Late response aliases a new transactionquarantine§12 — identity reused before it was safe
Table fills and stops acceptingleak§8's orphaned entries, or §21's drift
Failure cause is always the same generic errordiagnostics§20 — the last failure is overwriting the first

The fifth row is the one worth internalising. A transaction stuck in RESP_PENDING looks, from a system perspective, exactly like a transaction stuck in REMOTE_OWNED — software is waiting either way. The phase is the only thing that distinguishes "the far side owes us" from "we are not accepting what already arrived", and they have opposite investigations.

32. Debug Checklist

  1. When was the transaction allocated, and exactly once? §29's first check.
  2. Which identity, and which generation?
  3. What is its current phase? §31 routes the entire investigation from this one answer.
  4. Which layer currently holds responsibility? §4's third column.
  5. Is a transport object still retained for it? Note this can be no while the transaction is healthy (§27, cycle 11).
  6. Has remote ownership been observed? This is the fact that makes a timeout dangerous.
  7. Is a response expected, and has one arrived?
  8. Is the final consumer ready? If the phase is RESP_PENDING, this is the whole answer.
  9. Has retirement occurred, and after what? §14's table.
  10. Was the identity reused, and was it quarantined first? §12.
  11. Did a recovery occur, and in which phase? §24 — and check the table was untouched.
  12. Did a timeout occur, and in which phase? §16 — the phase determines whether reissue was safe.
  13. What was the first failure? §19 — not the last.
  14. Does txn_count_q equal the population of valid bits? §21, and it needs no model.
  15. Does the conservation equation balance? §23 — and remember a replay must not have incremented allocated.

Step 3 is the highest-yield question in the chapter. Six of the ten rows in §31 are distinguished by phase alone, and a design without a lifecycle state cannot answer it — which is the practical argument for building the state machine even where the functional logic could be simpler.

33. Common Misconceptions

"A transaction lifetime equals a packet lifetime." Four lifetimes, not one: the semantic transaction, the transport object inside it, each physical attempt inside that, and the response. The transport object's lifetime ends at confirmation with the transaction still open — often by many cycles (§3, §27).

"Replay creates another transaction." A replay is another physical attempt at the same transport object. Allocating on a transport event produces two live entries sharing one identity, one of which never retires, and the table fills with orphans under sustained retries (§8).

"Remote accept means the transaction can retire." The far side owes a response and nothing has come back. REMOTE_OWNED is not a terminal phase; it is the phase in which a timeout becomes dangerous (§14, §16).

"Response arrival means the transaction can retire." Arrival is not consumption. Between matching and consumer acceptance the response sits in a queue, and the transaction entry is the only record of what it is for (§15).

"Timeout means the transaction never executed." A timeout means no response arrived. Which of the six possibilities applies depends on the phase: from DISPATCHED a reissue is safe, from REMOTE_OWNED the operation certainly executed and a reissue duplicates it, and from RESP_PENDING there is no remote problem at all (§16, §17).

"All transactions should restart after link recovery." Recovery re-baselines link-epoch state — credits, replay entries, negotiated parameters. Transaction entries are not link state, and a transaction in RESP_PENDING needs nothing from recovery because its answer is already local (§24).

"Transaction state may be cleared on any local reset." Three reset kinds with three scopes. A local reset that also clears the transaction table has silently aborted every in-flight operation with nothing reported — and it works in bring-up, failing only when used as a runtime recovery (§25).

"ID reuse is safe once the request leaves TX." The identity's lifetime is the transaction's, and at transmission the transaction is three phases short of retirement. Releasing it there lets a late response alias a new transaction with a one-hot match, balanced counts, and a clean CRC (§13).

"A valid count proves the lifecycle state is correct." The count can be right while a transaction sits forever in an intermediate phase, which no in-run property fires on because residence is legal. That needs an end-of-test check that every allocated transaction reached a terminal phase (§23, §29).

"The final error code is always the root cause." Failures cascade: a transport error moves a transaction toward failure, the cleanup stalls, and a timeout overwrites the record. The diagnostic then names the symptom, and across many transactions the distribution of causes is destroyed as well (§20).

34. Understanding Check

35. Summary and What Comes Next

A transaction is an obligation with a lifetime. It begins when a layer accepts responsibility, it ends when the semantic obligation is resolved, and physical transmission is one event inside that lifetime rather than either end of it.

The distinction everything rests on: four lifetimes, not one. The semantic transaction; the transport object inside it; each physical attempt inside that, so a replay is not a new transaction; and the response, whose arrival is not its acceptance.

The mechanisms: one allocation at the semantic boundary, asserted per transaction with a verification tag because the protocol identity is reused. A legality function used by one property, plus a separate evidence property, because a legal transition can still reach a false conclusion. Entry actions rather than residence actions, or the transaction stuck in a phase has its age reset every cycle it is stuck. Retirement at consumer acceptance, enforced by a property written over the falling edge of valid so every clearing path fires it. Identity release as a consequence of a state transition, never of a transport event. Quarantine for failed transactions' identities, and a write-once first-failure field, because failures cascade and the last one is almost never the cause.

The insight that makes a lifecycle worth building at all: a timeout without a phase is an alarm with no information. From DISPATCHED a reissue is safe; from REMOTE_OWNED it duplicates an operation that certainly executed; from RESP_PENDING there is no remote problem at all. Same alarm, three opposite actions.

And the debugging consequence: six of the ten failure signatures in this chapter are distinguished by phase alone. A design without a lifecycle state cannot answer the highest-yield question in the checklist, which is the practical argument for building the state machine even where simpler logic would function.

The abstractions are now complete. Request ownership, response obligation, payload movement, and transaction lifetime have each been built separately. The final chapter of Module 12 removes the scaffolding and follows real transaction classes through the whole stack, cycle by cycle, with stalls, retries, coherence state, and retirement:

Browse the full path on the UCIe tutorials index.