-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
explicit guarantees on the finalized
event of submitAndWatch
#76
Conversation
I mean, if worse comes to worse and it's not possible to ensure this guarantee on the spec itself, then I will ensure that this guarantee is met at the low-level library that I've implemented for interacting with this JSON-RPC API. However, that to me feels a bit like a hack. |
IIUC, high-level APIs might need to synchronize the events produced by:
For example, the
Yep that makes sense. If both are present, capi is inspecting the extrinsics of the I always viewed the RPC-V2 classes of functionality as separate entities. This connects the two classes if they are both implemented making the implementation a bit complicated. I'm not entirely sure how we would intertwine the two APIs without complicating the code, since there are multiple variables that might lead to one API lagging a bit behind. One possible way to meet this may be to introduce a |
The third step (watching blocks) could also be done manually using the In order to implement that third step, smoldot takes some opinionated decision: if smoldot find the transaction in the body of block A, but can't download the body of block A's parent, it does not immediately report the transaction as being in block A. Instead, it waits until it is able to determine that the transaction is not in block A's parent before reporting that the transaction is in block A. If instead it finds the transaction in block A's parent, then it reports that the transaction is in block A's parent. However, this entire new API is designed around the idea that the client shouldn't take opinionated decisions when the JSON-RPC client is able to know better, and in this situation the JSON-RPC client is indeed able to know better. On the other hand, we kinda want the JSON-RPC API to be easy to use from CLI tools, so I'm not sure that it's a good idea to completely drop the "watching blocks" part of |
Yeah, I know. That's why I said that :
My concern is the redundancy. If the spec provided clear assurances about the timing and concurrency of these messages, there would be no need for me to replicate this effort.
Most transactions can only be inserted once in a chain, but the exact location of their inclusion can be vital. Here's why: For many dApp developers, when a new finalized block is known, the aim is to immediately understand all potential state changes associated with that block. This includes all events, storage calls, and any pending transactions contained within, alongside their errors and/or events. The CAPI-client intends to make this process easier by providing an API for which if the developer receives a message about a block, it should mean all relevant aspects of that block have been resolved. Different UIs might use this information in different ways. Some may hold off on showing users the new finalized block until all possible state changes have been computed. Others might show the block immediately but label certain information as "pending" until all state updates are confirmed. The user experience suffers when the UI rapidly updates and changes without the user having had time to digest those changes. To address this, I'll be refining the work on my low-level library tailored for the JSON-RPC API. However, I still believe it would be ideal if these guarantees were part of the spec itself. |
Offhand, I think I'd want the opposite guarantee. If I'm subscribed to blocks via The reason being that
I guess the advantage of
I guess the point here is that in rare cases it's possible to have the exact same transaction included in more than one block, and Smoldot makes sure to return the first block that such a transaction was included in at the expense of maybe being a little slower to guarantee this. In such cases, there is no "exact location" of the transaction, right? It was simply the case that I submitted a transaction, and then later that same transaction appeared in eg two separate blocks. My action could have led to either being true and I don't know. (Perhaps I'm totally wrong here but that's my understanding so far!) |
To answer this specific point: the way it's designed right now is that Either we keep it this way, or we remove the fields of |
I'd like to propose going one step further and eliminating the events Here's my rationale:
So, yeah... After reading all your comments I've come to realize that it would be best not to have these 2 events being produced from |
As I've explained above, this is not possible. The JSON-RPC server must continue broadcasting all transactions periodically until they have been included in the finalized chain. Broadcasting has no actual guarantee of delivery, a bit like a UDP message. Removing the tracking of blocks is possible only if it becomes the responsibility of JSON-RPC client to inform the server when the transaction has been finalized. |
It's the other way around, actually. Precisely because I want to unpin ASAP, I want to have this guarantee. The capi-client intends to provide an API which will allow it to automatically unpin blocks, because we will have the guarantee that the user has already requested all the stuff that they need from that block. So, imagine a situation in which I know that all the storage queries that the user wants to make for that finalized block have been made, and that the client has also extracted all the other information that it's relevant for that block and that the client has already provided that information to the consumer... So, logically now I want to unpin the block b/c it looks like I won't be needing it anymore, right? Well, wrong, b/c without the guarantee that I'm requesting I can't do that because maybe an event comes later saying: "hey, you know what, that transaction that was pending... guess what? it got finalized in that block that I told you before", but now I have unpinned the block and I can't make further requests to check which events/errors the transaction has produced. On the other hand, if I have the guarantee that the events produced from Without these guarantees, those events are useless to me, b/c I won't be able to unpin unless I have requested the body of the block and made sure that none of the pending transactions are present in it... which is exactly what I will end up doing, and the reason why I'm now suggesting to remove those events from |
I'm actually cool with this. I much rather this than what we currently have TBH. So, IIUC the way that it could work is that the JSON-RPC server would keep broadcasting until the client calls the Yes, please, let's do that! 🙏 EDIT: If we end up going down this road: it would probably be a good idea to rename |
I'm not super happy with "just removing the two events". As I've also mentioned above, this JSON-RPC API is not just about UIs. Use cases also include for example exchanges that want to submit transactions and have a guarantee that it's been finalized, or people writing utilities targeting their own node. |
I get your concerns about not wanting to simply yank out those two events. But, referencing the Objectives section of the spec, it’s clear: The JSON-RPC interface is primarily designed for intermediary library developers. Its complexity is intentionally traded off for making functions more explicit and predictable:
This is why I believe the spec should cater more to those building intuitive layers on top. Take our CAPI example: We first built a foundational client library. This isn’t just for UI stuff but can serve other non-UI projects effectively (like all the ones that you have mentioned, actually). I'm really hoping to see more of such foundational libraries, in various languages, sprouting up and enriching the ecosystem. I mean, let's be real – managing a transaction broadcast isn’t any trickier than efficiently handling the unpinning of Bottom line: I believe those events might add more noise than signal, and they seem to implicitly couple two distinct function groups. Since the libraries built on top will likely deal with these things anyway, why not place that responsibility squarely in their court? This shift would also allow libraries to address the needs of the non-UI scenarios you've highlighted. But hey, I already made my points, so if you choose to keep those events, I'll just pretend they aren't there 🙈. However, some clarity in the spec about the events’ timing, especially in relation to the |
Personally that's the opposite of what I'd want in Subxt though; I'd like to know that if I see a block hash in a response, that I can actually ask for details about that block. In Subxt, we let a user submit and watch a transaction, and when it's finalized, we take that block hash and use it to get the events to know whether the tx was successful or not (in the highest level API) and report that back to the user. If you get the "finalized" event first, the block may not be pinned yet per Perhaps we just have different notions of what the high level APis will look like, but in any case, I like the idea that if you're told about a block hash from the backend, that block is pinned (unless you've explicitly unpinned it already). I think that feels consistent.
Regardless of guarantees, since the node has to wait for the tx to be finalized anyway (or otherwise error out), it feels a bit pointless to not report those events to the user. Even if they aren't useful in one case, they may as well be provided, and if the client doesn't care then it can ignore them. And anyway, the client needs to know when it can stop caring about some "submitAndWatch" subscription, so the most you could do was to just remove the hashes from those events, right? Which seems a bit pointless to me (they can easily be ignored if not useful, and provide useful information in at least some cases otherwise) |
This applies specifically for the End-user-facing applications section of the page. The justification is that the JSON-RPC API is too difficult to use directly for them anyway, so there's no point in providing some functions that are easy to use, as there will always be other functions that aren't easy to use. For the
It is actually already mentioned under
There's not even a guarantee that the blocks are the same as the one yielded by It's not just a hypothetical thing. It can realistically be the case, if you use a load balancer and some of the load balanced nodes are a bit lagging behind, that the blocks produced by |
Regardless of any guarantees (and I understand now that they may not even be possible to provide, and anyway I'd want the opposite guarantee from Josep :D), I basically think that the events should stay, as is, because it's useful for the client to know when to stop listening for "submitAndWatch" events anyway (ie when it receives "finalized"), and then why not also tell it the block hash since that information is there? |
I mean, sure... but then how do you know when to unpin from a block? Because I'm sure that on Subxt you must also plan on unpinning ASAP, right?
So we do in CAPI, of course...
Not yet, but you are guaranteed that the Anyways, with the current spec you can not rely on one behaviour or the other b/c the events could come before or after, so, it's not like those events are going to be any useful to you either in that regard, right?
less ideal than not knowing when to unpin? I disagree, sorry.
I don't think so, no. I think that it boils down to the fact that I want to make sure that I don't stay pinned for longer than what's necessary, because well, if I don't have a good logic for unpinning then things will blow up, so yeah I care quite a bit about having the required guarantees so that I can safely unpin whenever I no longer need to use a block. You still have not explained what your solution for unpinning effectively is with the guarantee that you prefer, or with the lack of guarantees that we currently have. |
I will know when I should stop listening for "submitAndWatch" irregardless of those events, because I will do so when a transaction that has been broadcasted and that it hasn't errored appears in the body of one of the blocks reported by the |
Mmm, I have two competing ideas and need to decide on which approach I take. Roughly: Idea 1:
Idea 2:
I can see that with idea 1, a guarantee after would be simpler for me, and with idea 2, a guarantee before might be simpler! I'll probably be exploring idea 2 ultimately but may well start with 1, since it's a subset of 2 anyway (ie both will auto-unpin blocks that are reaching the end of their lifetime, and then 2 adds smarter unpinning on top). I'll start working on some of these things soon and will see what happens :)
Here we are in a different position, because your implementation will be light client only, and since Smoldot has downloaded the block bodies already (afaiu), there's no overhead with your API getting them all and doing that check manually. Subxt can connect to RPC nodes too, and in that case, having to download block bodies (which could be quite large) is much more expensive if it can be avoided by watching for the appropriate events and hashes instead and just following block headers otherwise. |
I'd like to address what seems to be a common misunderstanding regarding CAPI. We are not developing a "light client only" solution. Instead, our focus is on a "light client first" approach, and there's a significant difference between the two. The "light client first" philosophy means that our library is primarily built with the light client in mind. However, it isn't limited to just that. Our only expectation from the JSON-RPC provider is the proper implementation of the This is important for a number of reasons. One of them is that it enables a smoother migration for dApps, especially those using PolkadotJs. By allowing them to use both PJS (which will use the legacy JSON-RPC API) and CAPI (which will use the new JSON-RPC API) concurrently (while talking to the same server), the transition becomes possible. Once the migration is complete, dApps can then shift to a light-client provider exclusively. I had been under the impression that both CAPI and Subxt were aligned in this "light client first" direction. However, your latest comments suggest otherwise. While this is surprising and, in my opinion, perhaps a missed opportunity, I respect and understand that different projects may have their unique reasons and trajectories. |
Please do not worry about non-existing performance problems. We can solve performance problems later, for example by adding a JSON-RPC function that asks the node whether a transaction is in a body. The main objective right now is correctness and ease of use, performance in general really doesn't matter to me at this point. Block bodies can in theory be quite large. In practice they aren't. |
Subxt will eventually be light client first, but it's been around and used for a while and so is still using the old APIs (and for this reason also can't afford to "only" support the new APIs until they are stable, though it will have unstable support for them soonish). I also still think that there will always be valid use cases for being RPC-node friendly, such as if you run your own full node and want to talk directly to that or what have you, but eventually the light client interface (which we already support now, albeit via the old APIs and marked "unstable") will be the main one that we push :)
Sure, but I'll always tend towards using whatever APIs are made available in a way that is more performance friendly, and watching for a hash in a small event is more friendly on the Subxt side than downloading block bodies and checking. But, that said, I can see that downloading block bodies has a certain elegance to it though, especially in the light of the transaction call not offering any guarantees about the hash it reports, so I'll probably explore that approach too. It would also enable me to unpin blocks sooner because I don't have to keep anything around to match up to the hash that comes backm which is pretty handy! |
As I explained in this comment. I reached the conclusion that separating |
While working on the higher-level abstraction for submitting transactions, I realized that if the node also implements the
chainHead
functions, I don't need to stress about thefinalized
event intransaction_unstable_submitAndWatch
. I'd rather have one reliable source of information than juggle two that need to sync.What I really need is to be sure that when the
finalized
event happens, the block in that event matches the latest one from thebestChainBlockIncluded
event. So, I'd like the spec to be clear about this. It just makes things simpler and more foolproof.cc: @tomaka
EDIT: I'd also like to have the guarantee that if the node implements the
chainHead
functions, thebestChainBlockIncluded
event fromsubmitAndWatch
will come before thechainHead
finalized
event which includes that block in its list offinalizedBlockHashes
. (I just added a second commit with a note to be explicit about this guarantee).