Skip to content

feat: Add archive_unstable_transactionReceipt for efficient transaction lookup#182

Open
Nathy-bajo wants to merge 5 commits into
paritytech:mainfrom
Nathy-bajo:transaction_receipt
Open

feat: Add archive_unstable_transactionReceipt for efficient transaction lookup#182
Nathy-bajo wants to merge 5 commits into
paritytech:mainfrom
Nathy-bajo:transaction_receipt

Conversation

@Nathy-bajo
Copy link
Copy Markdown

@Nathy-bajo Nathy-bajo commented Jan 5, 2026

resolves #181

This PR implements archive_unstable_transactionReceipt to provide efficient, stateless transaction location queries without requiring full block downloads

@Nathy-bajo
Copy link
Copy Markdown
Author

Please review @bkchr @josepot

Happy New Year also!

Comment thread src/api/archive_v1_transactionReceipt.md Outdated
Comment thread src/api/archive.md Outdated
@josepot
Copy link
Copy Markdown
Contributor

josepot commented Jan 5, 2026

Happy new year @Nathy-bajo! 🎉

I’m with you on the main points:

  • Having to download block bodies just to check inclusion is pretty bad.
  • We definitely need an RPC that facilitates finding if and where a tx has been included.
  • And yeah, ideally the whole archive group of functions should be stateless (so it can be served over HTTP).

That said, I think there are a couple of real issues with this PR as-is:

1) This shouldn’t go into archive_v1

The spec is pretty clear that we shouldn’t add new methods to archive_v1.
This belongs in archive_v2. So IMO this PR should target archive_unstable first, while archive_v2 is being stabilized.

2) The API that you proposed is a bit off

2.a) Inclusion isn’t binary

A tx can show up more than once:

  • in multiple non-finalized forks,
  • in more than one finalized block (account removed after a transfer_allow_death, then receievs funds and re-submits, etc),
  • and in the extreme, even twice in the same block 🤯

I think the method should just return all occurrences (i.e: Vector<(block_hash, index)>, or something similar).

2.b) The output leaks business logic / FRAME assumptions

For instance, the proposed success/failed status punches into the business-logic layer. The archive RPC should ideally stick to data facts: which blocks and which extrinsic indexes contain the transaction.

Whether a transaction “succeeded” is not fully canonical at this layer:

  • Consumers can query System.Events (or equivalent) and interpret events to derive outcomes.
  • Even then, interpretation is FRAME- and call-pattern-specific. Example: proxy execution, if the proxied call fails, it won't bubble up a System.ExtrinsicFailed event, so the consumer will have to check into the inner-events anyways.

In other words: event interpretation belongs in a higher layer, not in the archive RPC surface. Same for all those other fileds: fee, gasUsed, logs, etc.


Given all that, I think the best path is: first solve this properly in chainHead_v2 (which should be the natural place for this kind of “tx landed where?” functionality), and then do the archive_v2 improvements. archive_v2 should be much easier once chainHead has the right building blocks.

On our side: as soon as our 2026 budget gets approved, we’ll be actively working on these fronts.

@Nathy-bajo
Copy link
Copy Markdown
Author

@josepot Thank you for the detailed feedback. I’ve reworked the solution as archive_unstable_transactionReceipt. It efficiently returns all transaction locations as an array of block/position data without FRAME assumptions. It also stays stateless over HTTP and sets things up nicely for future chainHead_v2(archive_v2_transactionReceipt) improvements.

@Nathy-bajo Nathy-bajo requested a review from bkchr January 6, 2026 14:37
@Nathy-bajo Nathy-bajo changed the title feat: Add archive_v1_transactionReceipt method to Archive API feat: Add archive_v2_transactionReceipt for efficient transaction lookup Jan 7, 2026
@Nathy-bajo Nathy-bajo changed the title feat: Add archive_v2_transactionReceipt for efficient transaction lookup feat: Add archive_unstable_transactionReceipt for efficient transaction lookup Jan 7, 2026
@Nathy-bajo
Copy link
Copy Markdown
Author

@bkchr @josepot Can I proceed with the implementation in the polkadot-sdk?

Comment thread src/api/archive_unstable_transactionReceipt.md Outdated
Comment thread src/api/archive.md Outdated
Comment thread src/api/archive.md Outdated
Comment thread src/api/archive.md Outdated
Comment thread src/api/archive_unstable_transactionReceipt.md Outdated
Comment thread src/api/archive_unstable_transactionReceipt.md Outdated
@lexnv
Copy link
Copy Markdown
Contributor

lexnv commented Mar 11, 2026

Would love to hear what use-cases you have in mind for this method?

Do you plan to use it for scanning different hashes in the full chain? Would you like to monitor different behaviors that might be too intense to execute from following the tip of the chain?

Ultimately, it feels to me that this behavior could be better achived locally by users:

  • run an archive node or download a snapshot
  • feed the blocks via a tiny script / adjust the archive node to extracts the required info hashmap<tx_hash, map<block_hash, index>
  • then expose a local RPC API or plain script that reads from those entries in the DB

Otherwise, this implementation might add too much pressure on archive nodes to provide a response, which might take a very long time in comparison with the local DB implementation.

@Nathy-bajo
Copy link
Copy Markdown
Author

Would love to hear what use-cases you have in mind for this method?

  • Wallets and DApps — they do not control the node. A user checks "did my transfer go through?" after reconnecting; they cannot run an archive node or a local DB scan.

  • Cross-chain bridges / relayers — a relayer needs to prove that an extrinsic was finalized before unlocking funds on the other side. They need a stateless, cacheable HTTP query against any archive node.

The "run a local script" path is valid for operators who control their own node, but this RPC is specifically for the large class of consumers who don't.

@Nathy-bajo Nathy-bajo requested review from bkchr and lexnv April 7, 2026 21:55
@Nathy-bajo
Copy link
Copy Markdown
Author

@lexnv please what do you think of the current spec?

@lexnv
Copy link
Copy Markdown
Contributor

lexnv commented Apr 15, 2026

Wallets and DApps — they do not control the node. A user checks "did my transfer go through?" after reconnecting; they cannot run an archive node or a local DB scan.

Could this be achieved via transactionWatch_v1_submitAndWatch instead? When the websocket connection drops, the wallet can reconnect/connect to a different RPC endpoint and then resubmit the transaction with the same nonce? Then the transaction either gets included or is simply rejected with an outdated nonce.

To determine the exact block, users can run a chainHead_follow subscription next to the transactionWatch.
The chainHead_follow subscription already provides the last 16 finalized blocks, which should be sufficient to scan the past ~ 1 minute 30 seconds of blocks.

When the network was down for more than 1.5 minutes, you could still fallback to using the archive API and scan any blocks from the moment you first submitted the transaction to the latest finalized to find the exact hash.

Cross-chain bridges / relayers — a relayer needs to prove that an extrinsic was finalized before unlocking funds on the other side. They need a stateless, cacheable HTTP query against any archive node.

IIUC, the relayers should know the block hash from the finality proofs. Then, they should only check for transaction inclusion in the specific block (archive_v1_body should be the most straight forward way to achieve that)

@Nathy-bajo
Copy link
Copy Markdown
Author

Nathy-bajo commented May 14, 2026

Thanks @lexnv.

Quick context: there was prior consensus on adding this method on the polkadot technical fellowship public chat (approved by @bkchr), so the question here is on shape, not on whether it should exist.

transactionWatch_v1_submitAndWatch requires being the submitter, returns yes/no instead of where, and is a stateful WS. No good for HTTP wallets, indexers, explorers, or anyone querying a tx they didn't send. The 16-block chainHead_follow window (~1.5 min) is also too small for mobile wallets reopened hours later or batch workers.

archive_v1_body still forces relayers to fetch and decode an entire block body to find one extrinsic. An index lookup is the right primitive.

Your DoS concern is fair. The revised spec makes the index opt-in via rpc_methods (also resolves @bkchr "nodes don't have this info"), requires a server-side count cap, adds a from height for bounded pagination, and drops the log field so the response is location-only.

Also reframed as a query, not a verification, per @bkchr . Trustless inclusion needs finality proofs or a light client. Renamed to findTransaction per your suggestion.

@bkchr
Copy link
Copy Markdown
Member

bkchr commented May 14, 2026

Quick context: there was prior consensus on adding this method (approved by @bkchr), so the question here is on shape, not on whether it should exist.

Where? I can not remember that I ever said this.

@Nathy-bajo
Copy link
Copy Markdown
Author

Quick context: there was prior consensus on adding this method (approved by @bkchr), so the question here is on shape, not on whether it should exist.

Where? I can not remember that I ever said this.

PHOTO-2025-11-16-09-04-23 2

@Nathy-bajo
Copy link
Copy Markdown
Author

Quick context: there was prior consensus on adding this method (approved by @bkchr), so the question here is on shape, not on whether it should exist.

Where? I can not remember that I ever said this.

PHOTO-2025-11-16-09-04-23 2

@bkchr

@bkchr
Copy link
Copy Markdown
Member

bkchr commented May 14, 2026

I would not say that this is a consensus :D

@Nathy-bajo
Copy link
Copy Markdown
Author

@bkchr Are you saying we don't need this method?

@Nathy-bajo
Copy link
Copy Markdown
Author

I would not say that this is a consensus :D

Fair, dropping the consensus framing. Every concern raised has been addressed in the current spec (opt-in indexing, query-not-verification, location-only response, fork-aware status, bounded pagination, renamed to findTransaction) so @bkchr is there a specific remaining concern that would block merging this as archive_unstable_findTransaction?

@bkchr
Copy link
Copy Markdown
Member

bkchr commented May 14, 2026

Best to get some feedback from @josepot.

As you can only use this with full nodes, I don't see that much value in them. Plus it will require that we store extra data per node to have this data available.

@Nathy-bajo
Copy link
Copy Markdown
Author

Nathy-bajo commented May 14, 2026

@bkchr Light clients can use this method trustlessly even though they can't serve it.

The flow: the light client calls findTransaction against any RPC to get (blockHash, transactionIndex), then verifies locally against the header it already follows from finality. Fetch the body via archive_v1_body, recompute the extrinsics root, check it matches the trusted header, and confirm the extrinsic at transactionIndex matches the user's bytes. The RPC acts as an untrusted index hint; the proof is local.

Without this method, a light client answering "where did my tx land?" has to scan every block since submission and pull each body, which is exactly the inefficiency this addresses.

On the storage cost: it's opt-in. Nodes that don't want to maintain the index don't advertise it in rpc_methods. RPC providers serving wallets and light clients can choose to pay the cost; nodes that don't, don't.

Will wait for @josepot's read too.

@Nathy-bajo
Copy link
Copy Markdown
Author

@bkchr Your argument in the Polkadot technical fellowship chat was that light clients should also be able to use this method, which the current design supports.

@bkchr
Copy link
Copy Markdown
Member

bkchr commented May 14, 2026

The flow: the light client calls findTransaction against any RPC to get (blockHash, transactionIndex), then verifies locally against the header it already follows from finality. Fetch the body via archive_v1_body, recompute the extrinsics root, check it matches the trusted header, and confirm the extrinsic at transactionIndex matches the user's bytes. The RPC acts as an untrusted index hint; the proof is local

This doesn't work, because you have no idea what a block_hash at some random point in the past was.

@Nathy-bajo
Copy link
Copy Markdown
Author

This doesn't work, because you have no idea what a block_hash at some random point in the past was.

You're right that finality alone doesn't give the light client a header at an arbitrary past block.

Two cases where verification still works trustlessly:

  • Inside the tracked finalized window against cached headers.
  • For older blocks by fetching the header plus a GRANDPA justification against a known authority set (the warp-sync primitive), then verifying body against header.

Beyond strict light clients, the actual consumers (wallets, indexers, explorers, relayers) already operate in an RPC trust model for everything else. findTransaction doesn't raise or lower that trust. It just makes a query they currently need to answer by scanning blocks one by one into an efficient primitive.

@Nathy-bajo
Copy link
Copy Markdown
Author

Hi @josepot, still waiting for your feedback on this please.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Archive API] Add transaction receipt query endpoint for historical transactions

4 participants