The GroveDB Structure
Drive keeps everything in one GroveDB: a tree of trees. The root layer holds
one subtree per RootTree variant, each of those holds fixed keys or one
subtree per identity, contract or token, and so on down to the items. The other
chapters of this section draw the part they are about. This chapter is about
the whole, and about the two things that keep a picture of it honest: a
description written as code, and a viewer that reads it.
Dive into a tree to go a layer down, use the breadcrumb or Backspace to come back up, drag the protocol version at the bottom to see what each version added, and switch a layer to Merk tree to see its real binary tree.
The description is code
The structure is declared in Rust, in drive::structure
(packages/rs-drive/src/structure). Each area declares its own part in a
structure.rs beside its paths.rs, from the same constants the paths use:
#![allow(unused)] fn main() { StructureNode::fixed( "balances", &[TOKEN_BALANCES_KEY], "TokenBalances", "TOKEN_BALANCES_KEY", ) .kind(ElementKind::BigSumTree) .since(9) .describe("Who holds how much of each token.") .child( token("The balances of one token; the sum is its circulating supply.") .kind(ElementKind::SumTree) .child( StructureNode::identifier("identity", "identity_id", "The holder's identity id") .kind(ElementKind::SumItem) .value("token amount") .describe("One identity's balance of the token."), ), ) }
A node says:
- its key: fixed bytes and the constant they come from, or a template such as "identity id, 32 bytes" standing for many keys;
- the element kinds that can sit there. Several when the code chooses, as the primary key tree of a document type does between eight tree kinds;
- the element flags on it. GroveDB stores a byte string of flags with
every element and never reads it; Drive keeps its storage flags there: the
epoch the element's bytes were paid for in, the bytes added in later epochs
if it grew, and for owned flags the identity that paid, which is who a
refund goes to when the element is deleted or shrinks. A node says whether
its element carries none, storage flags, or storage flags with an owner, and
who that owner is. The exported file explains each kind and its byte layout
once, under
flag_kinds; since, the first protocol version it exists in, which children inherit;- whether it is created with its parent, lazily on first use, or with its parent and deleted later while the parent stays, as an epoch's storage fee item is at payout;
- what an item holds, what a reference points to, and
recursewhere the structure repeats to any depth, as index levels do; - its source file and, when there is one, its chapter in this book.
The identifier of a node is the dotted path of its segments
(tokens.balances.token.identity). Identifiers are what two versions of the
structure are compared by, so treat them as append-only: renaming one reads as
a removal and an addition.
The module is compiled for tests and under the structure feature only.
Nothing in the node depends on it.
What keeps it true
The tests in packages/rs-drive/src/structure/tests.rs stand between the
description and drift. A lint first checks the description against itself:
identifiers are unique, reference and recurse targets exist, every source
file exists and names the constant a key claims to come from, and no two
templates of a layer could claim the same element.
Conformance. check_conformance walks a real GroveDB layer by layer and
reports every element no node describes, every element of a kind its node
does not list, every element whose flags are of a kind its node does not list, every element outside its node's protocol versions, and every
node that should have been created with its parent but is missing. Where
several templates of a layer accept a key, the one whose description fits what
is below the element wins: below a contested index a 32 byte key is a
contender's identity id at the last level and an index value before it, and
the key alone cannot tell. It runs against the initial state structure of
every protocol version, which pins each since to what
create_initial_state_structure really builds, and against populated state:
identities, contracts with documents and history, tokens, group actions,
address balances, an epoch before and after payout, contested documents. A
change that adds a root tree, a subtree key or a level fails here until it is
described.
The strategy tests. The drive-abci strategy tests check the state of every
chain they run with the same walker (assert_state_conforms_to_structure), at
whatever protocol version the chain ended on. They write far more than the
fixtures do: votes, withdrawals, token distributions, epochs changing, protocol
upgrades. Whatever a change's own strategy tests write is walked, so structure
created only during some operation is caught too.
Coverage. Every node must be reached by some rs-drive fixture or be listed
as reached by the strategy tests. UNVERIFIED, the list for nodes written from
reading the code and never checked against a real GroveDB, is empty and meant
to stay so: whoever describes a node can write a fixture that creates it. The
test also fails when a listed node does get reached by a fixture, so the lists
can only shrink.
The exported file. The description is serialized to
packages/rs-drive/grovedb-structure.json, which is what the viewer reads. A
test fails when the committed file is stale:
UPDATE_GROVEDB_STRUCTURE=1 cargo test -p drive --lib structure::tests
The shape of a layer
GroveDB stores each layer as a Merk, a balanced binary tree, and a proof of
one element carries the hashes along the way from the layer's root. So where a
key sits matters twice: keys near the top have shorter proofs, and every write
below a node rewrites its ancestors in the layer. That is why the root keys
are spread over the byte range with DataContractDocuments on top, and why
ContractGroups took key 124, which hangs below Versions, a tree only the
block-level version bookkeeping writes to (see
Contract Groups).
The exported file records the exact Merk of every layer whose keys are all
fixed, under layer_shapes. GroveDB does not expose the links between the
nodes of a Merk, but a proof does: the proof of a query for everything in a
layer lists every node and how they connect, so replaying its operations
rebuilds the tree. The viewer draws it when you switch a layer to Merk tree.
The shape depends on the order of insertion, not only on the keys. For a
layer reached through fixed keys only, the recorded shape is that of a fresh
chain at the latest protocol version ("origin": "genesis@14"). A chain that
upgraded through earlier versions inserted the same keys in another order and
can differ.
A layer below a template exists once per identity, contract, epoch and so on.
Its shape is recorded from one instance: the fullest one the test fixtures
build ("origin": "fixture contracts_with_documents@14"). That is where
layouts designed around the Merk show: a contract's layer keeps its documents
on top with the contract and everything else below, the other tree keeps
the banlist in the middle, and an identity's seven keys form a full tree with
the keys at the root. Another instance can differ when it holds fewer keys or
got them in another order.
Some layers gain and lose keys over their life, and then one shape is not enough. An epoch's layer is created at genesis holding only its storage fees; its first block adds the start fields, the proposers tree and the processing fees in one batch; and the batch that pays it out deletes the proposers and both fee items and writes the finished epoch info. No epoch ever holds all nine keys. A node declares such states in the order the layer goes through them, each with a title, what it means and what moves the layer into it, and the keys it holds then:
#![allow(unused)] fn main() { .state( "paid", "Paid out", "One batch pays the proposers, deletes the proposers tree and both fee \ items, and writes the finished epoch info.", &["start_block_core_height", "finished_epoch_info", "start_block_height", ...], ) }
The fixtures record one shape per state, and a state's shape must come from an instance holding exactly the keys the state declares. The viewer shows them as State 0, State 1, ... with their explanations. What matters for a faithful shape is that the fixture writes the same keys in the same batches as the block pipeline does, since a Merk batch of several keys gives another tree than the same keys written one at a time.
One document type's layout
The description covers every document type at once, so its document layers
are templates: an index property, one of its values, the [0] where an index
ends, and which tree types each of them can be. Which of those a given
document type gets depends on its keywords (unique, countable,
summable, the ranked keys, timeRange, indexOnly, documentsKeepHistory,
...), and the rules that pick them live in the index walkers.
drive::document::layout::document_type_layout applies those rules to one
document type and returns its concrete layout: the document type tree, the
documents by id, and for each index the property and value trees down to
where it ends, each with the tree or element type Drive writes, the
zero-contribution wrapper a continuation tree gets under an aggregating value
tree, the ranking axes of an indexed tree, the indexes that use the layer,
and conditions such as the tree a unique index falls back to when a value is
null. Each layer names the node of the description it is an instance of, so a
viewer can link to it. It needs only the contract, so it is compiled with the
verify feature as well, and the JavaScript SDKs expose it as
documentTypeLayout(contract, documentTypeName, platformVersion). It follows
the v2 index walkers, so it refuses a platform version before 14.
The tree types come from the functions the walkers call, and the element
choices the walkers make inline are held to them by
should_lay_out_what_drive_writes. It applies 19 test contracts (plain,
unique, compound, countable, summable, ranked and chained indexes, history,
time windows, indexOnly types with flat and preallocated indexes), inserts
documents, and fails on any element whose kind or wrapper the layout does not
predict, and on any layer of the layout Drive did not write. It does not cover
contested indexes, time windows with a ttl, or document updates and
deletes.
Pull requests that change the structure
When a pull request changes grovedb-structure.json, the
GroveDB Structure Preview workflow comments with a link that opens the viewer
on the difference between the merge base and the head of the pull request:
new nodes glow, removed ones stay as ghosts, every ancestor of a change
carries a count so the trail is visible from the root, and a tour walks
through each change. The viewer fetches both files from GitHub and compares
them itself, so the link works before the merge and for forks.
To add structure, follow the checklist in the coding conventions.