-
Notifications
You must be signed in to change notification settings - Fork 11.2k
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
RPC 2.0 #13700
Comments
Thank for sharing this major announcement. You may not be aware, but my team has implemented an indexer for Sui Object data. It also has a basic GraphQL interface. We were awarded a Sui Foundation grant and have been working on it for several months. Your project is much more ambitious, but it would be great to contribute. We really ought to have a call together and make a plan to coordinate efforts. Thank you. |
Introduce config for functional groups to enable and disable groups of features in the schema, so that different operators can choose to run the RPC service with different sets of features enabled (for example running public nodes without analytics). The precise set of functional groups may shift -- the current list is derived from the list in the RFC (#13700). This PR only introduces the config, and not the logic in the schema to listen to the functional group config. The config is read from a TOML file whose path is passed as a command-line parameter. This PR also brings the `ServerConfig` (renamed to `ConnectionConfig`) into the same module, and applies a common pattern to ensure there's a single source of truth for default values (the `Default` impl for the config structs). Stack: - #13745 Test Plan: New unit tests: ``` sui-graphql-rpc$ cargo nextest run sui-graphql-rpc$ cargo run ```
Introduce config for functional groups to enable and disable groups of features in the schema, so that different operators can choose to run the RPC service with different sets of features enabled (for example running public nodes without analytics). The precise set of functional groups may shift -- the current list is derived from the list in the RFC (#13700). This PR only introduces the config, and not the logic in the schema to listen to the functional group config. The config is read from a TOML file whose path is passed as a command-line parameter. It is stored as data in the schema's context so that it can later be accessed by whatever system limits access to endpoints by flags. A stub "experiments" section has also been added as a place to keep ad-hoc experimental flags (to gate in-progress features). This PR also brings the `ServerConfig` (renamed to `ConnectionConfig`) into the same module, and applies a common pattern to ensure there's a single source of truth for default values (the `Default` impl for the config structs). Stack: - #13745 Test Plan: New unit tests: ``` sui-graphql-rpc$ cargo nextest run sui-graphql-rpc$ cargo run ```
Introduce config for functional groups to enable and disable groups of features in the schema, so that different operators can choose to run the RPC service with different sets of features enabled (for example running public nodes without analytics). The precise set of functional groups may shift -- the current list is derived from the list in the RFC (#13700). This PR only introduces the config, and not the logic in the schema to listen to the functional group config. The config is read from a TOML file whose path is passed as a command-line parameter. It is stored as data in the schema's context so that it can later be accessed by whatever system limits access to endpoints by flags. A stub "experiments" section has also been added as a place to keep ad-hoc experimental flags (to gate in-progress features). This PR also brings the `ServerConfig` (renamed to `ConnectionConfig`) into the same module, and applies a common pattern to ensure there's a single source of truth for default values (the `Default` impl for the config structs). Finally, `max_query_depth` is moved from `ConnectionConfig` to `ServiceConfig` as it's a config that we would want to share between multiple instances of the RPC service in the fleet. Stack: - #13745 Test Plan: New unit tests: ``` sui-graphql-rpc$ cargo nextest run sui-graphql-rpc$ cargo run ```
Introduce config for functional groups to enable and disable groups of features in the schema, so that different operators can choose to run the RPC service with different sets of features enabled (for example running public nodes without analytics). The precise set of functional groups may shift -- the current list is derived from the list in the RFC (#13700). This PR only introduces the config, and not the logic in the schema to listen to the functional group config. The config is read from a TOML file whose path is passed as a command-line parameter. It is stored as data in the schema's context so that it can later be accessed by whatever system limits access to endpoints by flags. A stub "experiments" section has also been added as a place to keep ad-hoc experimental flags (to gate in-progress features). This PR also brings the `ServerConfig` (renamed to `ConnectionConfig`) into the same module, and applies a common pattern to ensure there's a single source of truth for default values (the `Default` impl for the config structs). Finally, `max_query_depth` is moved from `ConnectionConfig` to `ServiceConfig` as it's a config that we would want to share between multiple instances of the RPC service in the fleet. Stack: - #13745 Test Plan: New unit tests: ``` sui-graphql-rpc$ cargo nextest run sui-graphql-rpc$ cargo run ```
## Description Introduce config for functional groups to enable and disable groups of features in the schema, so that different operators can choose to run the RPC service with different sets of features enabled (for example running public nodes without analytics). The precise set of functional groups may shift -- the current list is derived from the list in the RFC (#13700). This PR only introduces the config, and not the logic in the schema to listen to the functional group config. The config is read from a TOML file whose path is passed as a command-line parameter. It is stored as data in the schema's context so that it can later be accessed by whatever system limits access to endpoints by flags. A stub "experiments" section has also been added as a place to keep ad-hoc experimental flags (to gate in-progress features). This PR also brings the `ServerConfig` (renamed to `ConnectionConfig`) into the same module, and applies a common pattern to ensure there's a single source of truth for default values (the `Default` impl for the config structs). Finally, `max_query_depth` is moved from `ConnectionConfig` to `ServiceConfig` as it's a config that we would want to share between multiple instances of the RPC service in the fleet. ## Stack - #13745 ## Test Plan New unit tests: ``` sui-graphql-rpc$ cargo nextest run sui-graphql-rpc$ cargo run ```
Sui RPC 2.0 Data Platform ArchitectureNoteThis post is not meant to be offensive or personal - it is strictly my technical opinion. I value everybody's work and perspective, but I'm going to call out some bad design decisions as I see them currently. I'm in this for the tech, I believe Sui is the best blockchain tech today. I'm really trying to contribute to keep Sui at the very top! Also, Please keep in mind that I have not been included in any discussions up to this point, so I don't have full context about the current plan at Mysten. Additionally, I am going to follow up with another post about Postgres, GraphQL, and the analytical indexer. IntroThe Sui RPC 2.0 proposal offers the opportunity to deliver a flexible, high performance, and cost-effective solution for engineers working with Sui data. Because developers must either issue RPC calls or manually scrape Sui data checkpoint-by-checkpoint, the community is eager for a new approach, which gives them the data they need in a timely manner. On the other hand, Sui fullnode operators currently benefit from the simplicity of the existing architecture. Each Sui fullnode can serve all RPC requests independently, which minimizes the infrastructure complexity. This also affords small teams the capability to run their own fullnodes with minimal overhead - a single node or a few nodes behind a load balancer can be quickly configured without any expertise in distributed data platforms. As the fullnode infra grows in complexity, there will be winners and losers in varying contexts. Having spent many years as a software engineer, many years as a site reliability engineer, and the last few years as an architect, I have dealt with data systems from every angle. I've felt the pain of slow API responses, rigid query patterns, inadequate documentation, and faulty payloads. I've been on-call for large, distributed databases after software engineers deploy unoptimized queries, bringing clusters to their knees and pulling me out of bed, late at night. I've also been at companies full of brilliant people, all of whom hate each other, because hastily designed data pipelines are so poorly implemented that nobody can actually identify the true values of specific data points. Section 1: What is An Indexer?Coming from a "big data" engineering background, the term "indexer" was something I only heard once I joined a blockchain team. It is a reflection of the chronic lack of expertise in architecture and data pipeline management in the blockchain industry. When blockchain engineers say "indexer" what they are really discussing is a system with several components. It is important to speak precisely about data management, because each of these components serves a different purpose, and different technologies are suited for each challenge. The Anatomy of a Change Data Capture PipelineIn data engineering, "change data capture" systems are implemented to synchronize data modifications from one data store to another. On Sui, this means synchronizing the "live" data stored in RocksDB - normally served via RPC - into a different datastore. This secondary datastore is only useful insofar as it improves one or more aspects of data access patterns - faster query times, lower compute costs, improved data models, etc. Real-time CDC pipelines synchronize the "live state" of the upstream datastore into a secondary database. In fact, several CDC pipelines may operate independently to feed different data - or unique representations of data - into several secondary databases. The secondary databases are used to APIs, dashboards, data science operations, etc. Depending on the query patterns required by these applications, different databases and models are chosen by an architect to achieve a specific outcome. To ensure the accuracy, timely delivery, and resiliency of data in the downstream databases, data engineers often utilize event streaming systems like Apache Pulsar, Kafka, or RabbitMQ. This decouples the data production system from the data consumption system, which ensures that data is never lost during deployments or unforseen outages. It also allows multiple software teams to consume the same data independently, model and enrich it as needed, and write to a database optimized for their application. In summary, a CDC pipeline involves an upstream datastore (source), a downstream datastore (sink), an event streaming platform, an app which publishes data updates from the source to the event streaming platform (producer), and applications to perform transformations and enrichment, then write to the sink (consumers). Section 2: Strengths and Weaknesses of the New RPC 2.0 Sui Indexer ArchitectureThe current Sui core code underway for the RPC 2.0 project includes two "indexers" which do not meet the criteria of a CDC pipeline: Benefit 1: Shared TypesOne of the strengths of the current design is that each component shares the same fundamental data types, laid out in the Benefit 2: Decoupled DatabaseMoving the database onto its own host(s) allows for predictable write performance on the core RocksDB datastore, helping keep the fullnode in sync. Depending on the database technology, it also allows for vertical and/or horizontal scaling to handle additional query volume, independently of the requirements of the core Sui fullnode / RPC server. This separation also facilitates operational integrity, but only in a carefully designed architecture. Benefit 3: The Hook System: sui_indexer::framework::interface::Handler;One of the largest changes in RPC 2.0 is the hook system. Developers may implement a Problem 1: Data AccessibilityDevelopers working with this handler must deploy their own fullnode in order to access this data. As the fullnode requirements grow, this will cause a larger and larger barrier-to-entry for teams working with Sui data Problem 2: Producer Data ResiliencyIf a developer must run a fullnode to access the data via a handler, what happens if that fullnode crashes? Or, even if there is a scheduled upgrade on the node? The developer must write code to account for missed events, writing complex "backfill" code and tracking which data has been ingested previously. In other words, the developer still needs to implement checkpoint crawling code, which leaves no benefit to using the handler. This is a very complex and finicky problem which many teams will simply brush over and accept some amount of data loss. Problem 3: Fullnode desynchronizationIn the original GitHub announcement, one of the problems mentioned by Mysten engineers is that "We found that fullnodes that have heavy read load can fall behind leading to serving stale data to clients." This is because the underlying Rust code operating on RocksDB often locks data during read operations. In the current implementation, each handler initialized will also initialize a Problem 4: Coupled Read and Write CodeThe reason a developer will implement a Section 3: Recommended Architecture - Event StreamingRather than forcing developers to implement their own The The fullnode.yml configuration file should be updated to determine which types will be published, and whether to include JSON, compressed, or both for each type. Developers may then consume the events from the topics relevant to their application, writing them to a downstream database of their choice. This is true for engineers at Mysten working with Sui core, as well as for teams indexing Sui independently. Tech Stack - Apache Pulsar as an ExampleAlthough there are many great choices, I will use Apache Puslar to exemplify the benefits of an event streaming architecture, in general. The important thing here isn't Pulsar - the point here is to see how an event streaming platform will resolve all of the problems discussed in Section 2. Solution 1: Data AccessibilityRPC providers can issue API keys for Pulsar topics and monitor resource consumption for each client (built-in feature). Developers never need to run fullnode infra, they simply consume messages from Pulsar. Solution 2: Producer Data ResiliencySince there is only one In addition, Apache Pulsar can (optionally) be configured to enforce data types on a per-topic basis. In this design, any producer which attempts to publish a malformed data point will be rejected. Administrators can monitor for rejected data points and trigger an alert. This ensures consumers will never recieve malformed payloads. Solution 3: Fullnode DesynchronizationSince there is only one Solution 4: Coupled Read and Write CodeBecause the Developers may implement independent consumer groups for each data sink, with no additional stress on the fullnode. For example, if a team needs to publish Sui objects to multiple databases, they would implement one consumer group per database, and they would all read from the same Pulsar topic. The producer would be unaffected, regardless of how many consumer groups are added over time. Additional Benefit 1: Developer FreedomBecause data is published to Pulsar as JSON, consumers may be written in any programming language. Developers do not need to write Rust to work with Sui data. For teams seeking to minimize latency, the compressed topic data could be used - depending on the format, various programming languages are supported. Additional Benefit 2: Operational ExcellenceConsider a scenario where Sui core is being updated, and the data producer (Sui Fullnode) must be restarted. This will cause an interruption in data flow on the producer side. Teams concerned with high availability may operate two or more fullnodes, each publishing identical data to Pulsar. The Pulsar topics may be configured to reject duplicate data, meaning consumers never have to handle deduplication. During the upgrade, one fullnode is restarted while the other continues to run. Once the node is back online, the next node is restarted. Data flow to Pulsar is never interrupted. Additional Benefit 3: IntegrationPulsar topic semantics are fully compliant with Kafka standards, meaning that any Kafka client may consume data from a Pulsar topic. In addition, Pulsar has several built-in data sinks, and integration with Apache Flink for advanced data pipelines. Countless cloud-based datastores integrate with Kafka and Pulsar as point-and-click data sinks. Next StepsLater this week, I will make an additional post discussing the Postgres data sink, GraphQL server, and analytical indexer. |
@sheldonkreger this is really really good feedback, thanks for thinking about them and taking the time to share your insights. I'd like to share some of my thoughts. First I want to acknowledge that despite of a lot of my time spent in this topic at mysten recently, I certainly have blind spots here and there throughout the entire thought process. I'd also use "I" instead of "we" below to avoid over-representing my teammates although I believe most of them are agreed upon. The PersonasWhen I initially thought about indexing blockchain data, I asked: who are we solving the problems for? I categorize them into two types:
The ValuesThere are a few values that I honor strongly in my thought train:
The High Level ArchitectureNow I want to share my thoughts on what the indexer should look like, why and how it differs from your recommendation. Note we will only talk about write path (indexing) here and omit the read path, as the former is the focus in your proposal. (a.) Sui-indexerThe graph below shows the diagram of running a sui-inexer. Sui-indexer gets checkpoint data from fullnodes via REST APIs. There are a few (b.) custom indexerThe graph below shows a custom indexer. This custom indexer could be written in any language (sui-analytic-indexer is an example of custom indexer, which happens to be written in Rust). Essentially it reads checkpoint data from fullnodes, using the same rest apis, runs their own customized logic and produces the indexed code. The logic can be as niche as possible, e.g. tracking whale wallets, monitoring NFT trading activities or defi TVL changes. There are two things I'd like to stress here:
(c.) streaming/messaging (@sheldonkreger 's recommendation)To make sure I fully understand your suggestion, I drew this: Here is a fullnode, and an indexer that processes all transactions, outputs the indexed data and pushes them into a queue. The users will subscribe to interesting topic to consume the data. If I understand correctly, the fullnode and indexer is run by professional node operators so the builders could focus on using the data directly. There are two highlighted points on my mind:
Does it mean I don't like message queue? Absolutely not, on the contrary again, I believe streaming offers arguably the best usability in many scenarios. Message queue definitely has a role to play in the blueprint: here comes another graph: (d.) from a RPC Provider's point of viewHere's the north star (end game) we envisioned, consisting of several elements we discussed above, including message queue. A buidler/user have the flexibility to choose different data sources according to what they are looking for. Resync? use data lake. Keeping up to the network? use message queue. Looking for some niche data? maybe they can find it in a certain storage. As I'm writing here, my realization is we didn't explain our end game plan as clearly as possible, and missed a few important puzzles here, including the data lake story. There is another route that we will go, which is a plugin system inside fullnode that allows you to operate the data (likely passing them to another server for heavylifting processing). This looks like Solana's Geysey if you are familiar with that. All in all, we should provide more details, likely in a separate post to avoid dilution. Answering Some QuestionsI hope it's a bit more clarified at this point, now allow me to discuss some detailed comments in your proposal:
Not necessarily, the developers could use a third party fullnodes if they can high tolerance of data integrity & centralization. And i believe it's the same group of people who will directly consume data from a streaming service or any data sources that drawn in graph (d).
This is true with all data sources, if I'm a serious builder. Because even the data source itself (say, pulsar) guarantees exactly once delivery, I don't want to blindly trust the data producer hasn't missed any data at all. So, a watermark will be required, the exact definition would needed to be agreed upon separately. Also, if one builds on top of sui-indexer, we guarantee no data would be missed by using strict watermark checking. This is handled in the shared code path so the builder does not have to. This is one of the place that takes care of it. (Please don't be scared by the huge PR, we're splitting them into a stack of smaller ones)
It's very true but is less relevant in this case. Previously fullnode was heavily swamped by certain types of queries because of the rocksdb performance issue. It has been better after some major improvements but still not doing the best. However, this is mostly due to fullnode directly serving the data traffic, meaning that any queries will hit fullnode and cause the embedded rocksdb to suspend in the unlucky case. This is the exact thing we will avoid in RPC 2.0, that is separating the read traffic from fullnodes, into somewhere that read is more efficient, such as PostgresDB and many other alternatives. You may wonder if the fetcher introduced here could add the burden for fullnode rocksdb? It's possible but I won't worry too much. Here is why:
Then who serves read quest? As written in RPC 2.0, the graphql server will.
I think this is somewhat implementation details, but usually we will recommend builders to separate the indexing code and commit code. For example, take this huge draft as an example again, here we have a Lastly I like many ideas you have around streaming (I'm more familiar with Kafka v.s. Pulsar) and configuration. Looking forward to your thoughts on Postgres, GraphQL and others! |
thanks @sheldonkreger for the detailed feedback!
the current indexer codes indeed has both writing and reading codes in the same trait, and I think we will want to separate them indeed. It's worth noting that in today's deployment, writer and readers are separately deployed to isolate the concerns.
a sweet-spot use case of MQ is like: a builder wants to read a subset of nearly fresh data, does ETL on the fly and renders sth on the frontend. As Lu mentioned above, 1) MQ is not great for bulk fetching, like fetching all first 1000 checkpoint data since genesis; 2) if the builder has to persist custom historical states, keeping the watermark is a minimum extension imo to ensure data integrity. with this in mind, I am thinking of message queue as a plug-in of "data providers" including fullnodes and indexers, where these data providers can be configured to open up queues for various topics when the sweet-spot use cases show up; Mysten can do a Geyser like configurable plug-in on fullnode, and maybe a pub-sub interface on indexer as well. Happy to chat more in the other proposal about the architecture! |
@sheldonkreger, thanks so much for the thoughtful, balanced, and educational feedback. We are absorbing all of this. |
Hello everybody - I haven't yet read through your comments, but I wanted to post my thoughts about the Postgres indexer and a few other things, since I've finished my review of that code. I will take a look and reply sometime this week. HUGE THANK YOU! NoteThis post touches the issues I see with the existing code base, the Postgres-backed "indexer", and briefly discusses a different solution for the GraphQL datastore. I belive that, as the project matures, more teams will need unique data representations, which will be best served by various database technologies, such as columnar, time series, graph, etc. But, a document database would be a good starting point, if GraphQL is the top priority. As a reminder, I do not have full context of what work is planned, and some of the issues I bring up are probably well understood and being addressed later. The Postgres IndexerThe fundamental thesis of this document is that the Postgres-backed indexer is unsuitable for production workloads for both current state and historical data. Data ModelThe Sui Objects are UnstructuredRelational databases are ideal for tightly structured data, where all fields are known at write-time. Each field can be written to a special column with a fixed data type, and relationships between entities across tables are managed with foreign keys. Although Sui Objects all share some fields - such as Storage of Serialized DataLooking at the schema for the Inadequate Index DesignBecause the actual object data is stored as Examples of Impossible QueriesConsider the following queries, which are very useful for a wallet, game, or NFT marketplace:
Because object data is stored as Operational Limitations of PostgresScaling PostgresAs more rows are added to tables in Postgres, queries require more resources and compute time to execute. Because Postgres does not support data sharding - or any form of distributed querying - all of the data in the database must fit on a single disk. Although it is possible to deploy read-only replicas to offload query traffic across several nodes, large tables are still cumbersome to query, especially without a strong index design. In a multi-node Postgres cluster, there is a single leader node which handles all write operations. Writes are eventually persisted to each read-only replica across the cluster. Large tables suffer notoriously from replication lag. Load Balancing and Primary Node FailoverQuery traffic must be distributed using an external load balancer. If there is an outage on the primary node which handles writes, the load balancer must detect this issue and redirect writes to a different node. Additionally, the standby server must be reconfigured as the primary server before it will accept writes. Therefore, clients issuing writes must be prepared to throttle write operations until the node has been reconfigured. Otherwise, data loss is inevitable. Materialized ViewsOne great feature of Postgres is materialized views. Briefly, a database operator may deploy a precalculated database query which reads data periodically and refreshes a value in another table. This is useful for replacing common queries which perform large aggreggations. However, data stored in Proposed Architecture: Document DatabaseDocument databases are designed to handle unstructured data, and are therefore the best choice for Sui objects. In our experience, maintaining a small collection size in MongoDB resulted in higher query performance and lower compute costs. Therefore, a document database could be used to serve the 'live data' - the latest version of each Sui object. Historical data, aggregate data, and and so on, may be better suited to another database type (columnar, time series, graph etc) - or even separate MongoDB collections. This is yet another reason to embrace the event streaming architecture described previously. Although there are several great document databases on the market, we will use MongoDB to exemplify the benefits of a document database. However, all document databases share most of these features, and selecting the best database is a different topic. This section is brief, only meant to propose the idea, rather than to architect the entire solution. Data ModelDocument databases do not enforce a schema at write time. Just like Sui objects, each document may contain unique fields and nested fields. These fields are instantly accessabile to query when they are written. There is no need to store serialized data in a document database. However, the serialized data can also be stored, if it is useful. In the Huracan Indexer, we stored all object data in MongoDB as its native data type, plus the serialized data for each object, in the same document. Index DesignBecause documents are unstructured, document databases rely heavily on indices for query performance. Any fields commonly used for filtering in queries should be indexed. Indices must be explicitly created, although MongoDB does have some utilities to automatically detect which fields should be indexed. In the Huracan Indexer, we created indices for object_id, object_type, owner, among others. Tightly structured types, such as Query ExamplesConsider the following queries, which are very useful for a wallet, game, or NFT marketplace:
Because object data is stored in the document, these queries are all possible. Operational BenefitsAll modern document databases are horizontally scaleable, where documents are sharded across several nodes. Data is stored in a primary replica and one or more secondary replicas, in a leader-follower scheme. In MongoDB, the query engine will automatically balance read requests across all nodes in the cluster. If read volume becomes too great, more replicas may be added to serve requests. One challenge of MongoDB architecture is ensuring that collections are sized optimally - again, a topic for another discussion. Although writes are sent to an individual leader node, clients may be configured to connect to all nodes in the cluster. If the leader node goes down, an election is held and a new node is selected as the new leader. Clients can be configured to automatidcally detect when this happens and quicky send write operations to the new leader. Polymporphism is the Wrong Pattern for Serving Sui DataIn the Because each For example, a columnar database like ClickHouse is designed to serve aggregatate queries rapidly. Fetching individual rows is much slower than it would be in a relational database, like Postgres. If the Instead, each Example - GraphQL via RPC InvocationsThe first data provider implemented in the |
Thanks for your feedback on this. I should flag that it's more useful to focus on the design proposed and not on the current code which is still experimental pending our data infra changes. |
@sheldonkreger , wow you again fascinated me with your deep thinking and rich experiences with different systems! I want to contextualize you so perhaps it's easier to see where we attempt to land and why. I'd break it down into three sections: Cost efficiency, Extensibility and Storage. Cost EfficiencyYour comment on unstructured data is 100% right. It's a pretty powerful on-chain feature that Sui uniquely has. However our realization is, it's prohibitively expensive to provide an unlimited set of indices on an unlimited data set. To give a quantitative sense, today on mainnet, there are over 205M live objects, not to mention the their older version and deleted objects. This number will grow indefinitely. As you said, the objects internally could be highly unstructured with fancy nested fields. Allowing this level of indexing has two major issues according to our past experiences, which help guide the new indexer architecture design:
One may argue that 4-6 digits per second is nothing as big tech internally runs much higher throughput, why are we so concerned? This is one of the major differences in blockchain data. In data rich companies, all the data in their ETL pipelines are valuable for them. But in an open blockchain, this is not the case. A gaming studio only cares about gaming metrics rather than Defi data. An NFT team have no desire to pay for social media app data. As a result, the professional RPC providers has no incentives to keep around data that is not going to generate business value. We want them to be profitable by operating on Sui, to make this a better ecosystem. In fact, this is a core value we honor throughout the decision process, as also mentioned in my previous reply. Let me give a concrete example on not having narrowly needed data in the main flow. Currently, the analytics data in Sui Explorer is served by indexer. This is baked in the main flow of indexer V1. It turns out to be not great, because active addresses and popular packages are not useful for most of the people, and it becomes a burden to maintain it in the main flow. That's why you see To sum up, we don't plan to have object fields indexed in the major tables in ExtensibilityThe new indexer design aims to provide high customizability and extensibility. As touched in my previous reply, you can have separated handlers or a custom indexer that indices data with your own logic.
This is also where we hope community can come together and participate in building composable infrastructure on Sui. DB SelectionAt this point you could even guess what i'm going to say :). We may end up having different data layers eventually. In (chart d) in my previous reply, the storage could be a relational DB, k/v store, document db and even a data lake. We are not going to implement everything on day 1, so we start from relational DB. To add some context, even though the syntax is Postgresql, the underlying databases could be anything as long as it's psql compatible. For example, today we are running in Aurora (Postgresql) because our old vendor has a hard limit in disk size (~5TB). Some offers would avoid the drawbacks you mentioned about the vanilla Postgres. In fact we are not even royal to Postgresql compatibility in the relational space. Ge @gegaowp is leading an effort to evaluate TiDB and CockroachDB as the new home. We also considered No-SQL databases briefly to start with, but it's hard to reason about consistency there, something we want to preserve in read. Regardless, I believe different data deserves different storage. With what you see in Thanks again for your feedback and time to reading! |
Thanks again for the detailed feedback @sheldonkreger ! @oxade and @longbowlu have already shared most of what came to my mind, but I'll add a couple of extra notes here and there: Data ModelIt's useful to start by describing our goals for the base RPC implementation. I don't think I did a good enough job describing that in the proposal, that's my mistake. We're aiming to offer an API that gives anyone visibility over the state of the entire network: If it happened on chain, you can find out about it through RPC, in near real-time. In previous iterations of the RPC, we introduced more and more expressive ways to query this data, but as the volume of transactions on the network increased, we found that some of these decisions hurt our ability to scale to handle that volume (both keeping up with throughput and storing all the data) and to reflect a consistent view of the network, which were factors we couldn't compromise on. When we revisited the schema for v2, this was top of mind for us, so we've made many decisions that limit expressivity to leave us room to scale while preserving consistency. The set of impossible queries you mentioned is a good case study to explore the implications of this:
The fact that you identified particular kinds of application (wallets, games, marketplaces) is interesting here. We also went through a similar process, but what we aimed to do was support the common denominators really well (the queries that everyone would need), and then rely on the various extension points to offer a way for:
In the three verticals you mentioned, we would expect:
The reason we don't support queries against object data is not because PG-compatible DBs cannot index this data -- indeed they offer the notion of a GIN (Generalized Inverted Index), which is similar to the posting list indices that document DBs use. We use this in other places, like indices on transactions, and we could have stored object data as JSON with a GIN to offer queries on fields, but we chose not to because of scalability concerns and not seeing a clear demand for this kind of functionality in the publicly operated RPC service.
This query is currently supported, using the
This query is also supported by the schema, using a GIN on
This is not to say that MongoDB couldn't do all this (I don't know it well enough to say if it can or not), but hopefully it helps identify the properties that we wanted to maintain (e.g. consistency, scalability) while improving expressivity, so we can discuss MongoDB (or other NoSQL/Document DB solutions) in those terms as well.
|
longbowlu Thanks for your response! I'll get to everybody, starting with yours. Thank you for the clarification about all of your plans. It really seems like the team is going in the right direction and that we generally share a strong vision for data access on Sui. All of the pros an cons you mentioned are insightful and I am in agreement with your conclusions. The personas and the values you mentioned are excellent, as well. Additionally, your understanding of my proposed architecture is pretty close, but allow me to clarify with a diagram. Apologies for not including this previously. Architectural Diagram
The key difference is that I suggest using an event streaming system as an intermediary between the data source (handlers using RPC invocations) and all downstream data sinks. This would allow for a single implementation of the handlers, and allow small, compartmentalized consumers for each data sink. Decoupling this code ensures resiliency, and it makes it easier to maintain custom transform/enrich logic for each data sink. Each consumer group can be updated or fail independently, and pick up where it left off in the stream. All of the other consumer groups remain unaffected. Backfilling via Checkpoints and RPC Invocations - Pain and SufferingFor the Huracan indexer, we implemented our own backfilling code which leverages countless RPC invocations. https://github.com/capsule-craft/huracan/blob/main/main/src/etl.rs Concurrent ingest workers are implemented with Tokio channels, which allows us to backfill several checkpoints concurrently. Because we are ingesting full object data as JSON and writing to MongoDB, we have a multi-step process:
In addition to the complex Tokio channel code, we also have to share state between channels to avoid re-reading data in flight in another channel. We use RocksDB to track checkpoints, transaction blocks, and objects that are in-flight with pending RPC requests. There's also some ugly pagination handling for te transaction blocks. We also have to handle graceful shutdowns using RocksDB. Each Tokio channel can be configured with a capacity to limit our RPC invocations and frequency of writes to MongoDB. You can see a demo of the configuration and deployment here: All of this was very complex to write. It will have to be re-written if there are changes to the RPC spec. Observability - despite my best efforts - is pretty difficult in Tokio. Managing state between threads is tough in any language. Frankly, I hope nobody ever has to write code like this ever again. It would be much easier to simply consume the data from an event stream, with the assurance that all of the data has been published by the sui-indexer system. Backfilling via Event Stream: EZ ModeRather than expecting every team to develop and maintain code like ours, something similar should be moved into Sui core. The Sui Indexer should include a small API which accepts requests for backfilling data, where the data is published to the event stream. In the Sui Indexer code, the backfilling data could be pulled from the data lake rather than via RPC. Or, the Geyser-style plugin you mentioned. Of course, builders could work directly with the data lake, but due to the volume of data they may need to request, I think it would be easier to manage on an event stream. Working with the data lake, builders would still need to handle query syntax for the data lake, pagination, and error handling during reads. An event stream avoids these complications. This is actually very similar to what you mentioned here:
Addressing Some Concerns
Very true! Having written custom backfilling code, I can say this is "easier said, than done." It seems like if I trust my RPC provider for RPC invocations, I should be able to trust them for data in their other APIs as well. But, this is a very valid concern.
100% agreed - Overall, offloading data reads is a huge win and even if developers are using the sui-indexer Questions and the pull requests
It sounds like there are some alternatives in progress: Data Lake, Geyser-style export system, and direct RPC invocations. Are there any others I'm missing? I will take a look at the pull requests you mentioned separately. Big thank you! |
gegaowp and sblackshear - Thanks for your reply. It sounds like you are thinking deeply about the challenges I mentioned already. Looking forward to more work together. Very impressed with the Mysten team. |
Thanks for the clarification. I sort of suspected it was POC. As I mentioned, I haven't been super involved on these conversations until recently, but I'm glad we're clarifying things now. I'm sure you're working on a more optimized solution, already. 👍 |
@amnn Thanks for your detailed response. I'm definitely seeing the 'bigger picture' with more clarity thanks to your post, and others. I think that one of the main challenges for the core Mysten team is deciding which data services to provide in the core, and which services should be handled by external vendors. Those kinds of details are VERY important in driving the decision about database technology. A really good example of this is where I suggest serving object-level fields, which you've considered and decided against it. You're obviously thinking about this with full perspective, and I have a lot of respect now that I see how much thought has gone into it. There's also the important consideration of operational complexity. Sure, a distributed database like MongoDB or Elasticsearch may offer a better query engine, but it's not necessarily fair to expect RPC providers to operate these in production. There's a reason why custom indexing services are a big industry in the blockchain world - things like this are "easier said than done." I also learned a thing or two - I was definitely wrong about Postgres support for nested field queries. Storing Sui objects as JSON and then creating a GIN is not something I had considered. In fact, it's my first time hearing of GIN in Postgres! I knew I had to be wrong about at least something in my lengthy rants 😆 That being said - With the Huracan indexer, I am already indexing into MongoDB and allowing some of these complex queries / filters, plus a GraphQL API. If you are ever interested, there are definitely some lessons I could share about the configuration of MongoDB, index design, and operational challenges of MongoDB. I've also operated massive ElasticSearch clusters, Kafka, Pulsar, InfluxDB, ScyllaDB, Graphite, among others. So, feel free to reach out if you ever have questions about operational techniques for distributed databases. It sounds like you've also spent a lot of time with these. One thing we are all agreement on is that extracting data and moving it to a custom datastore is a big challenge right now. Having written custom RPC scraping code, I just want to emphasize that I think this is an area that deserves a lot of attention, because everybody will benefit. As for the RPC-backed GraphQL setup, Oxade also mentioned that it's more of a POC. Thanks for the clarification. |
@longbowlu Responding to your comments about database tech here.
I'm glad to see the team is thinking about this. Your points are 100% valid and this was a major issue while I was indexing all Sui Objects into MongoDB with the Huracan indexer. There is no effective way to put all of the objects into a single MongoDB collection. Even with the indices, MongoDB collections really need to fit in-memory to avoid massive performance issues with disk I/O during reads. That's why I shut down ingest on the demo site. What I ended up doing was creating a whitelist/blacklist system for ingest, so you can select which package data is written to MongoDB. I build some general indices, but you'd also have to build more to fit your data. https://github.com/capsule-craft/huracan/blob/main/main/config.yaml#L116 In such a system, each team would need to run their own Huracan installation. That's fine for Huracan, but not an option for Sui Core. It would also be possible to put each type into its own collection - but I'd have to think about how this would affect the GraphQL implementation. Maybe the Huracan project isn't as dead as I thought it was when I saw the announcement about RPC 2.0. 😄
Based on your previous remarks and the explanation from @amnn , I think a relational DB is a great starting point. Now that I see the scope of what you're trying to accomplish, this makes a lot more sense. However:
This definitely indicates that you should be considering a database that scales horizontally. But, you probably already know 👍 |
This is a very insightful discussion, thanks everyone who is contributing! I'm not going to comment on high level stuff and architecture because it seems like you all know what you're doing, but just point out a few small things that came to mind reading this.
If I'm reading this correctly, data becomes available for reading on a checkpoint-by-checkpoint basis where writes commited within the newest checkpoint are not available until it gets fully committed? How does this work then with read-after-write consistency where I'm committing a transaction and then trying to read it? Will I have to wait for the checkpoint to get committed before I can issue a consistent read? +1 on "Reinterpreting Package IDs" and "Package Upgrade History", I already have use cases for this. Regarding devInspect / dryRun, will this new API make it easy to replay historical transactions? Currently you have to fetch the tx data and then replace any shared objects with their exact version it executed against from tx effects before running devInspect. This involves BCS marshaling / unmarshaling as tx data is returned in BCS and it's a bit cumbersome to do. In the current RPC, when fetching objects (and other things like events) with |
There's two ways to approach this -- the first is just to wait for the checkpoint which is the simplest option but will decrease your throughput by introducing checkpoint propagation and indexer post-processing to your critical path. This may not be so bad if your transactions involve shared objects because they need to go through consensus and appear in a checkpoint to be final anyway. The second option is to maintain the state you want locally, instead of relying on subsequent reads from the RPC. This approach is more complicated, but may be worthwhile if your transactions are otherwise operating in the single-owner fast path, and you care about your throughput. Typically what is important here is to know what the latest version of your owned objects are after having run some prior transactions on them, and this (the lamport version of a transaction accessing an owned object) is quite straightforward to forecast locally. While not in the immediate plans, we have discussed adding to our SDKs to do this automatically for you (i.e. the SDK keeps track of the owned objects you use in your transactions, and the versions they went in as and came out as, acting as a cache to avoid having to read versions from RPC in your critical path).
Dry Run is not the ideal tool for running historical transactions -- we actually have a separate tool for this, called
Indeed, here's what the schema for Move Objects looks like: sui/crates/sui-graphql-rpc/schema/draft_target_schema.graphql Lines 1015 to 1020 in e28d689
sui/crates/sui-graphql-rpc/schema/draft_target_schema.graphql Lines 180 to 192 in e28d689
It offers BCS and also a structured format. It is generally much more geared towards programmatic use cases. There was also a proposal to introduce a slightly more human friendly option (a generic JSON format where structurs appear as JSON objects, vectors as JSON arrays, etc). |
This seems somewhat complex. Keep in mind that to have a consistent app state on the UI you need object data as well and, if I'm not mistaken, the RPC doesn't return that in transaction response (just versions). So it would not be possible to do this unless you have the RPC return the updated object data in the response or have some way of simulating tx execution on the client side. Maybe a crazy idea but compile Move to TS and simulate exeuction locally? Seems doable if you have consistent object data already in your cache, but you'd still have to fetch dynamic objects. |
You're right, this is quite complex, which is why we wanted to abstract this away in the SDK as much as possible. The scenario I was describing (around tracking versions) primarily applies to backend services that might need to deal with a lot of concurrent transactions, but the case of optimistic updates to UIs is an interesting one (and it's a separate scenario). Luckily, I think you don't have to go as far as running Move code as TS -- you can leverage dry run to get optimistic object updates (still not currently straightforward, because object changes are represented as raw BCS, but should be easier than transpiling Move to TS!) and this is not really extra work, because in most cases a transaction will need to be dry-run to estimate gas costs. There are still going to be edge cases where this kind of optimistic update is not accurate (think of a transaction that relies on an oracle whose data changes often), but this is an inevitability. |
I believe you might be underestimating how tricky these things are on the frontend. It's a delicate balance between code complexity and network code efficiency. You need to display same underlying data in different ways in different parts of the UI. React code has a hierarchical structure where data is fetched at top components and being passed down. So naively, you can be very network efficient and fetch every piece only once in one place but then code complexity will come from having to pass it around hierarchically unrelated components. Or, naively, you can have simple code and fetch data in any place it's needed. But then you're slamming the RPC with requests. So what you do instead is have an underlying caching layer so that you can have simple code of "fetch data where it's needed", but have the fetch hit the cache. Doing an optimistic update naively will not work well because any subsequent read on the RPC will overwrite the state with the old one if the checkpoint didn't have a chance to go through yet. This can cause flickering on the UI and old state to be displayed after the transaction confirmation message and crypto users are very sensitive to that because they get rugged all the time. Having to refresh the page all the time or wait 3 seconds is not great UX. Existing frontend libraries don't handle this because they don't have to deal with this type of inconsistencies on the backend (read after write is normally consistent while here it's normally incosistent). My strong feeling is that you should prioritize having something in the SDK to help with this. I'm not optimistic about app developers dealing with this on their own. |
To clarify, my comment about whether this was extra work or not is purely in terms of whether this kind of solution requires an extra roundtrip or not, optimistic updates absolutely is a tricky thing to get right. Let me also cc @hayes-mysten and @Jordan-Mysten who are looking into dapp-kit etc which seems like a good home for patterns to solve this. |
This is definitely getting into interesting territory. The GraphQL ecosystem has a lot of tooling around various kinds of normalized cache for front end applications. Several GraphQL libraries have solutions for optimistic updates in the GraphQL cache, and usually support keys derived from multiple properties (id + version) with various invalidation strategies. The tricky piece here is that I think we want to avoid blessing a specific GraphQL solution in dapp-kit, because some graphql solutions end up dictating how your entire app is structured. There are a lot of tradeoffs to consider when choosing a GraphQL library. I'm not sure how this will end up looking. We may be able to come up with some more general helpers that can be used with multiple libraries, or add several library specific utils allowing you to pick what you need. This is definitely something that will need more exploration before we can commit to a specific path. |
Sui's objects and transactions are schema-free json. Once I want to suggest using the mongodb to store the data. Now GraphQL is available and I strongly support the proposal. |
Hey! I've been exploring new RPC 2.0 and found only trivial examples that don't help me to solve the following non-trivial, yet seemingly simple task: to consistently load all I firmly believe that addressing such a challenge should be prioritized as one of the primary objectives for the new API. To facilitate this, could you please provide a GraphQL request example that demonstrates how to efficiently load all For start, you can think of use sui::table::Table;
struct Pool has key, store {
asks: Table<u64, Table<u64, Order>>, // Please note that `Order`s live at the second tier of dfs!
bids: Table<u64, Table<u64, Order>>,
} and we need to load all |
Hi @nikolai-aftermath, the main thing that makes this complicated is the wrapping (the thing you've highlighted in your comment: "Please note that Today, the indices backing the GraphQL API do not look "inside" objects, which is what is preventing us from handling this kind of request in a single query. Their aim to give a general view of all data on-chain and we tend to recommend using custom indexers for domain-specific indexing needs (where the data being indexed lives inside the structure of an object), and that would be the most direct way to solve this problem. (And here, I mean "custom" from the perspective of the tables backing the GraphQL service -- it might still be something we offer as standard, given that deepbook is part of the system packages, it just would need us to do additional indexing). In future, we will support wrapped object indexing, which might alleviate this requirement, if we exposed an ability to iterate over all the objects that are wrapped in another object, and then run sub-queries over them. If I needed to solve this today, I would run three separate queries:
query GetPool($poolId: SuiAddress!) {
object(address: $poolId) {
contents { json }
}
}
# Paginated query to get all elements of a table that must be used in two stages. Once to get all the prices,
# and another to get all the orders at those prices. We use the `owner` API to get the dynamic fields of a
# wrapped object (like the tables in question).
query IterateTable($tableId: SuiAddress!, $after: string) {
owner(address: $tableId) {
dynamicFields(after: $after) {
pageInfo { hasNextPage endCursor }
nodes {
name { json }
value { ... on MoveValue { json } }
}
}
}
} If you do this in a straightforward way, it will not necessarily be consistent, because when you start each iteration, it will take a snapshot of the query at the checkpoint the iteration started on. Technically speaking, you could carefully craft a query GetPool($poolId: SuiAddress!) {
checkpoint { sequenceNumber }
object(address: $poolId) {
contents { json }
}
}
query IterateTable($tableId: SuiAddress!, $after: string, $checkpoint: Int) {
owner(address: $tableId) {
dynamicFields(after: $after, latestAt: $checkpoint) {
pageInfo { hasNextPage endCursor }
nodes {
name { json }
value { ... on MoveValue { json } }
}
}
}
} When you run the first query, to get the IDs of the |
Hey, @amnn, the hypothetical and unattainable future you describe is of course beautiful. It would be nice if we had some sort of method But even the worthless API you propose doesn't work at the moment: query {
owner(address: "0x029170bfa0a1677054263424fe4f9960c7cf05d359f6241333994c8830772bdb") {
dynamicFields {
pageInfo { hasNextPage endCursor }
nodes {
name { json }
value { ... on MoveValue { json } }
}
}
}
} returns error
Here 0x029170bfa0a1677054263424fe4f9960c7cf05d359f6241333994c8830772bdb is the address of SUI-USDC pool bids leaves table. |
Thanks for the report -- we are working on addressing the timeouts at the moment (cc @wlmyng) -- there are some performance optimisations and query planning improvements that are still in progress. Regarding the hypothetical |
Note
RPC 2.0 has launched, the most up-to-date version of its docs can be found on the Sui Docs.
Motivation
We’re excited to share a proposal re-imagining Sui’s RPCs (front- and back-end), optimizing for:
Summary of Changes
The biggest user-facing change is that RPC 2.0 will offer a GraphQL interface, instead of JSON-RPC. GraphQL offers a better fit for Sui’s Object Model, comes with established standards for extensions (federation, schema stitching) and pagination (cursor connections), and a more mature tooling ecosystem including an interactive development environment.
On the back-end, the RPC service and its data-store will be decoupled from fullnodes. Fullnodes’ APIs will be limited to transaction execution and data ingestion for indexers, with all read requests served by a new, stateless RPC service, reading from its own data store. Indexers will consume transaction data from fullnodes in bulk, post-process them and write them to the store.
This redesign also offers an opportunity to address many known pain points with the existing RPCs such as deprecating the
unsafe
transaction serialization APIs, and providing more efficient query patterns for dynamic fields, among other usability issues reported by users of the current RPC.Timeline
By end of October 2023, we will release an interactive demo supporting most queries in the schema linked in the GraphQL section to follow. This service is offered as beta software, without SLA, primarily intended for SDK builders to target ahead of a production release. It will not support transaction execution or dry runs and will operate on a static snapshot of the data which will be periodically updated as new features in RPC 2.0 are implemented.
By end of December 2023 the first version of the new RPC will be released as 2024-01.0, at the end of the fourth quarter of 2023. This version of the RPC will support all MVP features in the proposed schema and it will be deployed by Mysten Labs and shared with third party RPC providers for integration into their services. There will be an opportunity for RPC providers to provide feedback on the service architecture and for their customers to provide more feedback on how the API is to use which we will aim to incorporate into future versions of the RPC (released quarterly).
Support for the existing RPC will continue at least until end of Q1 2024, to give time to migrate. Until that time, changes to the existing RPC will be kept to a minimum (barring bug-fixes), to avoid disruption. We will assess whether there is sufficient support for GraphQL in the ecosystem, before we sunset the existing RPC.
Versioning
RPCs will adopt a quarterly release schedule and a versioning scheme of
[year]-[month].[patch]
(e.g.2023-01.0
,2023-02.3
, etc). Breaking version changes are reserved for new[year]-[month]
versions, while patch versions maintain interface backwards compatibility.This replaces the current scheme which ties RPC version to fullnode/validator version (which can update weekly). Decoupling node and RPC versions allows RPC to evolve at its own pace and differentiates breaking RPC changes from breaking node changes, and even changes to the indexer that processes data for the RPC to read (which will be versioned separately).
Setting Versions
API versions can be supplied as a header, not including the patch version (e.g.
X-Sui-RPC-Version: 2023-10
). If a header is not supplied, the latest released version is assumed. The response header will include the full version used to respond to the request, including the patch version (e.g.X-Sui-RPC-Version: 2023-10.2
).Deprecation
Each RPC major version will receive 6 months of support for bugfixes. Publicly available Mysten-operated RPC instances will also continue to provide access to an RPC version for 6 months after its initial releaes. Clients that continue to use versions older than 6 months will be automatically routed to the oldest supported version by the public Mysten RPC instances. E.g. clients who continue to use
2023-10.x
past the release of2024-04.y
will automatically be served responses by2024-01.z
to limit the number of versions an RPC provider needs to support.When deprecating an individual feature, care will be taken to initially make changes in a schema-preserving way, and reserve breaking changes for a time when usage of the initial schema has dropped. When deprecations remove fields, subsequent interface changes will avoid re-adding the field with new semantics, to reduce the chances of an unexpected breaking change for a client that is late to update.
GraphQL
A draft of part of the schema follows, giving a flavor of what the new interface will look like. The design leverages GraphQL’s ability to nest entities (e.g. when querying a transaction block, it will be possible to query for the contents of the gas coin). Fields will be nullable by default, to leave flexibility for field deprecations without breaking backwards compatibility. Pagination will be implemented using using Cursor Connections with opaque cursor types:
For a more detailed look at the proposed schema, and to follow its development, consult
draft_schema.graphql
or the snapshot of the schema currently supported by the implementation.Extensions
The ability to add extensions to the RPC is a common request. Apps may require secondary indices, and RPC providers often provide their own data to augment what the chain provides.
GraphQL offers multiple standards for schema extensions (e.g. Federation, Schema Stitching) and even multiple implementations of those standards (e.g. Apollo Federation, Conductor) that offer the ability to seamlessly serve an extended schema over multiple services (which could be implemented on different stacks).
Mysten Labs will offer a base RPC service implementation that supports the same functionality as the existing indexer. Functionality will be split into logical groups (see below) which can be turned off for a given deployment to reduce CPU and storage requirements. This implementation will be compatible with a schema extension standard (but will not require one to work out-of-the-box).
Logical Functional Groups
UpgradeCap
s, tracking popular packages.Interface Changes
Unsafe APIs
The
unsafe_
APIs, responsible for serializing transactions to BCS (e.g.unsafe_moveCall
,unsafe_paySui
,unsafe_publish
etc) will be removed in RPC 2.0. SDKs that depend on these APIs for transaction serialization will be offered a native library with a C interface to convert transaction-related data structures between JSON and BCS to replace this functionality.See Issue #13483 for a detailed proposal for this new library.
Dynamic Fields
The
1 + N
query pattern related to dynamic fields was a common complaint with the existing RPC interface: Clients that wanted to access the contents of all dynamic fields for an object needed to issue a query to list all the dynamic fields, followed byN
queries to get the contents of each object.This will be addressed through RPC 2.0’s use of GraphQL, which allows a single query to access an object’s dynamic fields and their contents:
Using this API, the following query can be used to page through the contents of all dynamic field names and values for a given object:
Dry Run and Dev Inspect
The existence of both a
dryRun
and adevInspect
API has been a source of confusion, as they offer overlapping functionality. RPC 2.0 will combine the two into a single API to provide the behavior of both without overlap:The combined API can be used to replicate the current functionality of dryRun and devInspect as follows:
Data Formats
The number of input and output formats will be limited to maintain consistency across API surfaces:
0x
on output (but will be accepted in a truncated form in input).Clients that depend on truncated package IDs in outputs and numbers represented as Doubles will need to migrate to the new data formats while adopting the new API.
Types that are represented using BCS on-chain (such as
TransactionBlock
s,TransactionEffect
s andObject
s) will offer a consistent API for querying as BCS, to cater to clients that relied on the BCS or Raw output functionality in the existing JSON-RPC.Data Consistency
Currently, clients that need read-after-write consistency use the
WaitForLocationExecution
execution type. This guarantees that reads on the fullnode that ran the transaction will be consistent with the writes from that transaction, however:The new interface will do away with
WaitForLocalExecution
and provide a blanket consistency guarantee for all its data sources (i.e. all data returned from a single RPC request will be from a consistent snapshot).To enable this, the RPC’s indexer will need to commit writes as a result of checkpoint post-processing atomically, which increases latency (transactions that are final may take longer to show up in fullnode responses). A Core API will be provided to query which range of checkpoints the service has complete information for:
Typically, last will be the latest checkpoint in the current epoch (modulo some post-processing latency), and first will be determined by RPC store pruning. All APIs are guaranteed to work above first, some may continue to work when serving data based on checkpoints below it.
Consistency and Cursors
Paginated queries (using the Relay Cursor Connection spec) will also support consistency. For example, suppose Alice has an account,
0xA11CE
, with many objects, and we query their objects’ IDs:After issuing this query, Alice transfers an object,
O
to Bob, at0xB0B
, so at the latest version of the network, Alice no longer owns an object that they previously did own. However, paginating the original query by successively querying:Will iterate through a set of objects that is guaranteed to include
O
, regardless of whether the object was transferred before the page containing it was fetched or after. This ensures that queries that run over multiple requests still represent a consistent snapshot of the network.This feature depends on the RPC service having access to data from historical checkpoints, which may be pruned (e.g. if the historical checkpoint has a sequence number lower than
availableRange.last
). If the checkpoint is pruned, cursors pointing at data in that checkpoint will become invalidated, causing subsequent queries using that cursor to fail.The history that is retained in the RPC’s data store is configurable. Publicly available, free RPC services will aim to retain enough history to support queries on cursors that are a couple of hours old, whereas paid services can support older historical queries.
This kind of consistency only applies on a cursor-by-cursor basis, so if we run a similar query for Bob, in a separate request, after the transfer from Alice:
And later paginate both sets of cursors:
Then object
O
will appear in both Alice’s object set (paginating through cursors that were initially created before the transfer) and Bob’s object set (paginating through cursors that were created after the transfer), so although both sets are self-consistent, the overall query may not represent a consistent snapshot.Limits and Metering
The flexibility that GraphQL offers comes with a risk of handling more complex nested queries, which could consume too many resources and result in a bad experience for other RPC users. This will be addressed through limits that are configurable per RPC instance:
Mysten-operated, publicly available RPC endpoints will be configured with conservative limits (and transaction rate-limiting) to ensure fair use, but other RPC providers are free to adapt the limits they offer.
Service Architecture
This new service architecture is intended to remove the fullnode from the data serving path as well as providing a solution that lends itself more towards scalability.
Data will be ingested from a fullnode to a customizable indexer for processing. From there indexers can do any required processing before sending the data off to various different storage solutions. The image below shows one such indexer storing blob data (raw object contents or raw transactions) in a key/value store while sending relational data to a relational database. From there we can have any number of stateless RPC services running in order to process requests from clients, grabbing data from the requisite data store in order to service each request.
Some of the motivations for removing the fullnode from the data serving path are as follows:
Fullnodes may still expose a limited API, e.g. to submit transactions, query the live object set, etc, for debugging purposes but the bulk of traffic is expected to be served via instances of the RPC service.
Data Ingestion
In order to facilitate third-party custom indexers and data processing pipelines we’re designing and implementing an Indexer Framework with a more efficient data API between a FN and an Indexer.
The framework is built to allow for third-parties to build their own
Handler
s which contain custom logic to process and index the chain data that they care about. At the time of writing, the trait is as follows:The latest version of this trait can be found here. Running a custom indexing pipeline involves:
enable_experimental_rest_api
config totrue
in itsfullnode.yaml
file.Handler
trait (above)Handler
orHandler
sFurther Work
There are some additional known improvements that we want to add to the RPC, but have been reserved for future releases:
Filtering Dynamic Fields by Key Type
On Sui one package can extend another package’s objects using dynamic fields. Objects that are designed to be extended this way can collect a number of dynamic fields of completely unrelated types and applications that extended an object with one set of types may only be interested in querying for dynamic fields with those types. Augmenting the dynamic field querying API with filters on the types of dynamic fields will allow applications to achieve this without over-fetching dynamic fields and filtering on the client side:
Wrapped Object Indexing
Wrapped objects (objects that are held as fields or dynamic fields of other objects in the store) present similarly to deleted objects in RPC output, and consequently in Explorer too. This causes confusion, when an object is available but not by querying its on-chain address.
Wrapped object indexing tracks the location of objects that are embedded within other objects so that RPC can “find” an object’s contents even when it is wrapped. Similarly, it can be used to detect
Bag
s andTable
s to improve their representation in Explorer as well.Reinterpreting Package IDs
If an upgraded package includes a new type, that type’s package ID will match the upgraded package’s, but types in the same package that were introduced in previous versions will retain their package IDs.
This complicates reads with filters on type: Constructing such filters requires clients to keep track of the package that introduced each type. This can be simplified using an implicit cast: A type can be supplied as
0xA::m::T
and will be cast to0xD::m::T
where0xA
and0xD
are versions of the same package, with0xD
being the greatest version less than or equal to0xA
to introduce the typem::T
.This process saves significant book-keeping for clients who can now refer to all the types in a particular package by that package’s ID, and not by the IDs of the packages that introduced them.
Improvements to Dry Run
Display
output on objects in Dry Run.Package Upgrade History
Currently, it can be difficult to track all the versions of a package, as each version has its own Object ID. This situation can be improved with dedicated APIs for fetching all versions of a specific package.
Verifiable Reads from Fullnode
Although the proposed architecture decouples the RPC service, indexer and storage layer from fullnodes, an RPC provider is still currently required to ingest data only from a fullnode that they trust (which often means RPC providers run their own fullnode).
A trustless interface between indexers and fullnode (where the indexer could verify the integrity of data it reads from any given fullnode) will remove this requirement, as it eliminates the risk that a node run by an adversary could “lie” to an indexer for its own benefit.
API for Exposing RPC Limits
Feature request from @FrankC01 for pysui.
Some of the validation steps that the RPC performs on transactions can be replicated on the client, by SDKs, to avoid sending requests that are guaranteed to fail. Not all validation can be moved to the client (for example, it’s difficult to predict timeouts, or estimated query complexity), but this does not diminish the value of avoiding hitting other limits ahead of time.
Facilitating this feature in SDKs requires exposing information about limits in its own API. The core RPC implementation will include the following parameters:
Node providers are free to extend this with their own limits, to help SDKs avoid hitting their domain-specific limits.
The text was updated successfully, but these errors were encountered: