Skip to content

[BUG] getTransaction can return cached sent after CKB RPC reports rejected #378

Description

@phroi

CKB RPC treats tx_status as meaningful even when transaction is null. CCC currently drops those responses in JsonRpcTransformers.transactionResponseTo, so Client.getTransaction can fall back to a cached sent response after the node has already reported a terminal rejected status.

This makes confirmation waiters miss terminal failures such as Resolve failed Dead(OutPoint(...)) or RBFRejected(...) and eventually time out instead.

CKB RPC behavior

CKB documents and implements this shape intentionally:

  • verbosity=1: transaction must be null.
  • verbosity=2: transaction is returned only when tx_status.status is pending, proposed, or committed; otherwise transaction is null.
  • A rejected transaction is represented by tx_status.status = "rejected" plus tx_status.reason, with transaction = null.

Source evidence:

CCC behavior

CCC currently treats transaction == null as if the RPC response has no usable status:

So this sequence can happen:

  1. sendTransaction(tx) succeeds and caches tx as sent.
  2. CKB later reports get_transaction(txHash) as tx_status.status = "rejected", tx_status.reason = ..., transaction = null.
  3. CCC transforms that RPC response to undefined.
  4. Client.getTransaction(txHash) falls back to the cached sent response.
  5. A caller polling for confirmation continues to see a pending-like status instead of the terminal rejection.

Public testnet examples

These public testnet hashes currently show the CKB RPC shape:

{"hash":"0x97ef4b066dcd82a94e2a77b9c0d57027393b40ad6ef81892ad651c51de2f9a78","status":"rejected","reason":"{\"type\":\"Resolve\",\"description\":\"Resolve failed Dead(OutPoint(0xb9d8f14129f32c65029ec84b6f9e315d1e5fccf430b2adc0016a2031fc5f439d00000000))\"}","transaction":null}
{"hash":"0x5dfa0e353214b083f7fe54b1fb774e65f21f60a1fdf548efc0e3664e8a859f9c","status":"rejected","reason":"{\"type\":\"RBFRejected\",\"description\":\"RBF rejected: replaced by tx Byte32(0x1872347f07644f061a48de77559b5abf60049414b5f18083368cd2a29c47e2a2)\"}","transaction":null}

The same RPC with verbosity = "0x2" returns transaction content for a committed tx, but not for rejected txs:

{"hash":"0x5d7c726635abff46908b119ef9b82af8e04418827fbfbb8fe49fded705636352","status":"committed","transaction":"present"}

Expected behavior

CCC should preserve terminal tx_status and reason from CKB RPC even when the transaction body is absent.

At minimum, callers should have a CCC API that can distinguish:

  • RPC returned no useful record / unknown.
  • RPC returned a status-only terminal record, such as rejected with a reason.

Possible fixes

I am not sure which API shape best fits CCC's public types, but these seem plausible:

  • Change ClientTransactionResponse or its transformer to support status-only responses.
  • Add a status-only method, for example getTransactionStatus, backed by get_transaction verbosity 1.
  • Make getTransaction avoid cache fallback when the fresh RPC response contains a status-only rejection.

The key invariant is that transaction: null should not cause CCC to discard tx_status.

Impact

Applications that call sendTransaction and then poll getTransaction can miss terminal chain or pool rejections. In our live iCKB supervisor runs, this surfaced as confirmation timeouts even though public CKB RPC already reported rejected with either Resolve failed Dead(OutPoint(...)) or RBFRejected(...).

Keep up the Great Work,
Phroi %37

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or request

    Fields

    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions