By: Ramzi El Yafi
In our previous blog post, we discussed what a notary is, and why it plays a fundamental role in securing Corda’s UTXO ledger. In this blog post, we delve into the details of the notary architecture in Corda 5, delivering a solution which allows new notarization protocols to be introduced with ease, whilst also allowing finer-grained scaling of notarization resources, reducing total cost of ownership.
To begin with, let’s explain some key notary concepts in Corda 5, which are fundamental to understanding how a notary operates.
Notarization and uniqueness checking
Broadly speaking, notarization in Corda is formed of two parts. The first part is a flow protocol, in which a client virtual node requesting notarization establishes a session with a notary virtual node, which then performs protocol specific validation. The second part is uniqueness checking, which is fixed function logic that guarantees the integrity of the UTXO ledger by preventing double spends. The second part of the logic is invoked as part of the flow protocol.
This split can be observed in the notarization protocols of previous Corda versions. The non-validating and validating notary protocols differ in terms of the validation performed by the flow protocol, with the non-validating protocol performing only minimal checks to ensure that a transaction’s structure is correct, whilst the validating protocol performs contract validation against the full transaction back-chain. What these protocols have in common is that they both invoke the same uniqueness checking logic to prevent double spends.
This functional separation is retained in Corda 5, but is now better reflected in our underlying architecture, leading to a clear separation of responsibilities. We will explore this in more detail later.
Corda 4 supports two notarization protocols – validating and non-validating. The choice of protocol is made on a per-notary service basis by a network operator, and is done by specifying a Boolean flag as part of the notary definition in configuration. These protocols are hard-wired into the Corda 4 runtime, and it is not possible to define a different protocol.
Corda 5 takes a different approach, by defining notarization protocols as “plugin” CorDapps. These plugins take the form of a “client” component, which is bundled as part of “application” CorDapps, and contain an initiating flow to make a notarization request to a notary. A “server” component is installed as part of a notary CorDapp, which lives on notary virtual nodes, and contains a responder flow to process the notarization requests. This process means that unlike in Corda 4, CorDapp developers and notary operators need to make decisions about which notary protocol(s) they wish to support. However, the network operator still has the final say in these decisions, as they must decide which virtual nodes and notary services they will allow on the network. We will cover these topics in more detail in a subsequent blog post.
As previously mentioned, the MGM maintains information that links a notary service to a specific notarization protocol. This information is signed by the network operator, and then distributed to network members. This allows the appropriate protocol to be selected by virtual nodes at notarization time.
Notary services and notary virtual nodes
It is worth explaining what a notary service is, and how this differs from the notary virtual nodes that represent the service. The Corda 5 model for notaries is similar to the High Availability Notary mode available in Corda 4, with a few differences.
In Corda 5, notaries are always referenced using a notary service x500 name. Effectively, all notaries are treated as though they were high availability notaries in Corda 4. This means that CorDapps request notarization by specifying a notary service x500 name. The Membership Group Manager (MGM) defines the notary services that are available on a network by broadcasting each notary service’s x500 name, as well as the notary protocol supported by the service, and the notary virtual nodes that act as representatives for the notary service.
Notary virtual nodes are similar to regular, “application” virtual nodes; they are registered on the network, run flows, have their own databases, keys, x500 name, and holding identity. The only difference is that these virtual nodes run a special notary-specific Corda Package Installer (CPI) that contains a different set of flows to the application (non-notary) virtual nodes, allowing these virtual nodes to respond to notarization requests. We will go into more detail about notary application packaging in a subsequent blog post.
NOTE: For clarity, we will continue to refer to non-notary virtual nodes as “application” virtual nodes throughout this blog post.
Imagine a network with three application virtual nodes – Alice, Bob, and Charlie. There is also a single notary service on the network, with two notary virtual nodes representing the service. Such a network may appear as follows:
|Virtual node x500 names (used for communication between virtual nodes)||Notary service x500 names (used by application virtual nodes when requesting notarization)|
|Notary Representative A1||Notary Service A|
|Notary Representative A2|
We will cover how messages are resolved from a notary service to a representative later on.
Multiple virtual nodes in a high availability world
If you have read the blog post on High Availability in Corda 5, one may ask why we continue to use a notary service abstraction to allow multiple virtual nodes to represent a notary service, given that a single virtual node is itself highly available in Corda 5. Typically, we may expect that there will be a single virtual node representing a notary service, and indeed this will be the only option for the Corda 5.0 release. However, maintaining this abstraction allows for a notary service to be provided by multiple Corda clusters in different geographical regions, providing even greater resiliency.
The life of a notary
Now that we have explained the key notary concepts in Corda 5, let’s take a look at the lifecycle of the notary. This can be broadly split into the following stages:
- Creation of virtual nodes that will represent a notary service
- Network registration
- Servicing of notarization requests
Let’s look at each of these in turn.
Virtual node creation
As previously mentioned, notary virtual nodes behave in much the same way as application virtual nodes. The mechanism for creating a notary virtual node is the same as that of an application virtual node, with only two minor differences:
- They are associated with a notary CPI instead of the application CorDapp CPI.
- They have an additional “uniqueness” database, which stores data relating to unspent and spent states on the network. Unlike other virtual node databases, this database is shared across all notary virtual nodes representing a notary service, to ensure they have a consistent view of states on the network.
Returning to the example network setup we discussed earlier, this network would appear as follows:
Each database listed above may not necessarily be a different physical database; they may be different schemas within the same physical database.
Note that at this point, we have only talked about notary virtual node creation, not notary services. Notary services come into play as part of network registration.
Registering a notary virtual node follows the same process as that of an application virtual node. However, several additional pieces of information are provided in the
context of the registration request:
"corda.roles.0" : "notary"– This flags the virtual node as taking the role of a notary on the network.
"corda.notary.service.name" : <x500 name>– This is a user-specified x500 name for the notary service that this virtual node will represent. This is the name that will be used by CorDapps when specifying which notary to use for notarization.
"corda.notary.service.plugin" : <protocol name>– This is the name of the notary protocol that the service supports. This effectively replaces the
validatingBoolean flag in Corda 4, and allows flexibility in writing new notary protocols going forward. We will discuss this in more detail in a future blog post.
Effectively, the notary service is a concept that exists only in network metadata; the notary service springs into life once the first notary virtual node representing a notary service has its membership request approved. At this point, the notary service appears in network membership information, and can be used by the application CorDapp in notarization requests.
Once a notary service is visible to the network, an application requests notarization in much the same way as it did in Corda 4; when invoking finality logic, the CorDapp specifies the notary service it wishes to use for notarization. At this point, the Corda flow framework has two decisions to make:
- Which notary protocol to use.
- Which virtual node to send the request to, given that the messaging layer (P2P) only understands virtual nodes, and not notary services.
Both of these decisions are handled transparently by Corda, and require no intervention by a CorDapp developer. Selecting the notary protocol is straightforward; the application CPI will contain notary client CPKs for the protocols the application supports. When notarization is requested, the client node looks up the MGM metadata for the notary service, and searches for the notary client CPK that matches the specified protocol, which is then invoked.
Virtual node selection is similarly straightforward. The MGM metadata also contains the list of notary virtual node representatives for the notary service. These are selected in a round-robin fashion, with the initiating node requesting notarization sending flows to the selected notary virtual node, which will then start its corresponding responder flow.
The responder flow which runs on the notary virtual node does not initially do anything special; it can perform whatever logic it likes, as a normal responder flow would. In the case of the non-validating notary protocol, this logic is minimal and simply performs sanity checking of the integrity of the transaction that was received. Once this logic has been performed, the flow then invokes uniqueness checking functionality, which amongst other things, checks to ensure no input states on a transaction have already been spent, and will only spend them if all uniqueness checks pass.
In Corda 4, uniqueness checking is performed against batches of transactions in a separate thread to flow processing, in order to improve performance by reducing the number of round trips to the database. This batched approach remains in Corda 5, but the implementation is somewhat different. Rather than running a separate thread, uniqueness checking is handled by a dedicated uniqueness processor, which lives as part of the database worker. A notarization flow requests uniqueness checking by invoking a service which places messages on the Corda message bus, which are then picked up by a uniqueness processor.
A uniqueness processor will pull a batch of requests from the message bus. Unlike Corda 4, these requests may be from different notary virtual nodes, representing different notary services. To cope with this, the uniqueness processor will partition each batch of requests into sub-batches based on the notary virtual node that made the request, ensuring that the correct uniqueness database is used for each notary virtual node, and that there is appropriate data segregation between different services. The responses are placed back on the message bus to be picked up by the flow. The flow will then sign successful notarization requests with its notary key.
This approach has a couple of benefits. Firstly, it ensures proper separation of responsibilities; the uniqueness processor has exclusive access to a notary virtual node’s uniqueness database, but no keys or access to the other databases. Secondly, this allows flow and uniqueness resources to be scaled independently; if we are running a heavyweight notary flow protocol that is resource intensive, we can scale up the number of flow workers whilst running a modest number of database workers. If the reverse is true, we can instead scale up the number of database workers. We may even choose to separate out the uniqueness processor into its own worker if such a setup would be beneficial to performance. In the diagram above, we see an example of a single uniqueness processor handling requests from multiple flow processors.
In this post, we have explained the new notary architecture in Corda 5, and provided a rationale for the changes that have been made. In the next blog post in this series, we will go into more detail on the notary plugin CorDapp architecture, and discuss what this means for CorDapp developers and network operators.