Skip to content

Commit fcacc64

Browse files
feat(drive): compute what creating a document costs for the SDKs (#5159)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 99bc968 commit fcacc64

20 files changed

Lines changed: 4440 additions & 200 deletions

File tree

‎book/src/SUMMARY.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
- [Fee System Overview](fees/overview.md)
3535
- [Platform Address Fees](fees/platform-address-fees.md)
3636
- [Shielded Transaction Fees](fees/shielded-fees.md)
37+
- [What a Document Costs](fees/document-cost.md)
3738

3839
# Error Handling
3940

‎book/src/fees/document-cost.md‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# What a Document Costs
2+
3+
`drive::document::cost` computes what creating one document costs, from the
4+
contract alone. The JavaScript SDKs expose it as
5+
`documentCreateCost(contract, documentTypeName, options, platformVersion)`,
6+
which is what the contract visualizer shows per document type.
7+
8+
Amounts are in credits: 1 Dash is 100,000,000,000 credits.
9+
10+
## Storage
11+
12+
The storage fee is the bytes the insert adds times
13+
`storage_disk_usage_credit_per_byte` (27,000 credits from protocol version 14).
14+
It is usually almost all of what a document costs.
15+
16+
The estimate is exact. It lists every element the insert writes (see
17+
[GroveDB Structure](../drive/grovedb-structure.md) for the layout), built with
18+
the functions the insert walkers use:
19+
20+
- the document by id, or with `documentsKeepHistory` a tree of revisions and a
21+
pointer to the newest;
22+
- for each index, the value trees along its properties (created only when no
23+
earlier document has the value), the `[0]` terminal and the document's
24+
reference in it, or an indexOnly type's entry;
25+
- the rows a ranked index keeps for the value in its secondary trees, one per
26+
ranking axis;
27+
- the trees preallocated for other document types' `preallocated` indexes
28+
whose entries will reference the document, charged to its creator.
29+
30+
Each element is priced with GroveDB's byte formulas: the key with the 32-byte
31+
subtree prefix, the serialized element (or a fixed size standing for a tree),
32+
the value and node hashes, the aggregate feature of the tree it sits in (8 or
33+
16 bytes in a count or sum tree), and the link its parent keeps to it. A
34+
document create's elements carry 35 bytes of storage flags (the owner and the
35+
epoch); index trees carry them only when the documents are mutable, the
36+
contract can be deleted, or the type is indexOnly and its documents can be
37+
deleted.
38+
39+
The storage depends on what is already stored, so it comes in two scenarios:
40+
41+
- **every value new**: the first document with these index values creates
42+
their trees;
43+
- **every value known**: a later document with the same values adds only its
44+
own entries. A unique index still adds its value, which no earlier document
45+
can hold, as does every tree keyed by the document's own id (a preallocated
46+
index's, say) and a `ttl` document's expiration entry; a ranked index's row
47+
for an existing value only moves, which GroveDB bills as replaced bytes.
48+
49+
The estimate also splits the storage by index: the layers an index shares with
50+
other indexes (a common prefix of properties, paid once for the document) and
51+
the layers only it uses, so an index's cost on its own is its shared layers
52+
plus its own.
53+
54+
The test `should_price_what_drive_charges` inserts documents into 19 contracts
55+
covering every index shape and requires the estimate to equal the storage fee
56+
Drive charged, byte for byte, with the trees already stored read from GroveDB
57+
before each insert.
58+
59+
## Processing
60+
61+
- **Exact:** verifying the signature (15,000 credits for an ECDSA key, 300,000
62+
for BLS) and fetching the signing key and the identity's balance (18,000).
63+
- **Estimated:** the work of the writes (seeks, hashing and rewriting the path
64+
to each new element in its tree and above), for an assumed number of stored
65+
documents, each with its own values (1,000 by default); and the small reads
66+
and writes around the insert (the document id check and the identity's
67+
contract nonce). The test
68+
`should_estimate_the_processing_of_the_writes_within_a_factor_of_two` holds
69+
the write estimate to Drive's processing fee. Processing is a few percent of
70+
a document's cost.
71+
- A `userFeeIncrease` raises the processing fee only.
72+
73+
The per-document "minimum fee" of a batch (`document_batch_sub_transition`) is
74+
a balance check before processing, not a charge, and the unique index checks
75+
and the fetch of the batch's own contract are not billed.
76+
77+
## What the contract adds
78+
79+
- **Action fees** (`actionFees`): the create's fee, in credits, scaled by the
80+
epoch's fee multiplier unless priced as fixed, paid into the contract's fee
81+
pots.
82+
- **Token cost** (`tokenCost`): tokens transferred to the contract owner or
83+
burned.
84+
- **Contest fund**: a contested index's vote fund (0.1 Dash, doubling past 250
85+
contenders), paid when the value is contested.
86+
87+
## Refunds
88+
89+
Deleting a document refunds the storage fee of its flagged elements, less what
90+
the epochs already passed were paid: about 99.9% in the epoch it was created,
91+
about 95% a year later, then less each year for fifty years.
92+
93+
## Documents with a `ttl`
94+
95+
A document whose type declares a [`ttl`](../contract-keywords/ttl.md) is
96+
stored without flags and pays for its bytes by the lifetime it has left, all of
97+
its `ttl` when it is created, at the schedule's tier for that lifetime (from 1
98+
credit per byte for an hour to 26 for a week, then 34 per 788,400 seconds)
99+
instead of the 27,000 of storage kept for good. It also adds its entry to the
100+
documents expirations tree, and prepays its deletion as processing (a base
101+
cost, a cost per index level and one per document byte). Nothing of it is
102+
refunded.
103+
104+
## Not covered
105+
106+
- A create whose value starts a contest (a DPNS name matching the contest
107+
rule, say) is stored in the contest's vote poll until the contest ends, not
108+
in the index. The estimate prices an uncontested create and lists the contest
109+
fund; the vote poll's storage is not priced.
110+
- A platform version whose insert methods differ from protocol version 14's is
111+
refused: other versions write other elements for some shapes.

‎packages/js-evo-sdk/README.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Evo SDK provides a high-level, strongly-typed interface for interacting with [Da
2020
- [Immutable properties (`immutable`)](#immutable-properties-immutable)
2121
- [Property constraints (`propertyConstraints`)](#property-constraints-propertyconstraints)
2222
- [How a document type is stored (`documentTypeLayout`)](#how-a-document-type-is-stored-documenttypelayout)
23+
- [What a document costs (`documentCreateCost`)](#what-a-document-costs-documentcreatecost)
2324
- [Chained queries (provable semi-join)](#chained-queries-provable-semi-join)
2425
- [Composite queries (a page plus its sub-queries)](#composite-queries-a-page-plus-its-sub-queries)
2526
- [Contributing](#contributing)
@@ -473,6 +474,31 @@ const { root } = documentTypeLayout(contract, 'review', new PlatformVersion(14))
473474

474475
`structureNode` names the layer of Drive's GroveDB structure description it is an instance of, as the [GroveDB structure viewer](https://dashpay.github.io/grovedb-structure-viewer/) shows it (`#/<structureNode>`).
475476

477+
## What a document costs (`documentCreateCost`)
478+
479+
`documentCreateCost(contract, documentTypeName, options, platformVersion)` returns what creating a document of a type costs, in credits (`creditsPerDash` of them make one Dash), computed locally by Drive from the contract:
480+
481+
- `storage`: the bytes the insert writes and their fee, exact, under two scenarios: `newValues` (the first document with these index values creates their trees) and `knownValues` (a later document with the same values adds only its own entries);
482+
- `indexes`: per index, the bytes of the layers it shares with other indexes and of its own, so its cost on its own is `sharedBytes + ownBytes`;
483+
- `processing`: the signature and identity fetch (exact) and the work of the writes (estimated for `existingDocuments` stored documents);
484+
- `contractCharges`: the create's action fee, token cost and contest fund, when the type has them (a contested create is stored in the vote poll until the contest ends; that storage is not priced);
485+
- `refund`: what a delete refunds, in the same epoch and a year later;
486+
- `fields`: how the priced document was filled.
487+
488+
The document is built from sizes, not values: by default each variable-size field is at the middle of its bounds and each optional field is present. Pass `fields` to change that:
489+
490+
```ts
491+
import { documentCreateCost, PlatformVersion } from '@dashevo/evo-sdk';
492+
493+
const cost = documentCreateCost(contract, 'note', {
494+
fields: { text: { length: 200 }, mood: { present: false } },
495+
existingDocuments: 10_000,
496+
}, PlatformVersion.latest());
497+
const dash = cost.totalCredits.newValues / cost.creditsPerDash;
498+
```
499+
500+
It follows protocol version 14 on; an earlier version is refused. A type whose documents have a `ttl` is priced by lifetime, with no refund.
501+
476502
## Chained queries (provable semi-join)
477503

478504
A `refersTo: permanentDocument` declaration also lights up the read side: a **chained query** answers `SELECT * FROM post WHERE $id IN (SELECT postId FROM like WHERE $ownerId = me)` in one verified round trip. The node returns the inner indexOnly page and the referenced documents under ONE merged proof — a single quorum-signed state root by construction — and the SDK re-derives the outer query itself and checks it against the *proven* inner values — the node cannot substitute, omit, or inject joined documents. For a `permanentDocument` join property a missing referenced document fails verification outright, since such a reference cannot dangle. For a `deletableDocument` join property a referenced document that was deleted since is proven absent: it has no entry in `outerDocuments` (so match the two halves by id, not by position) and its id is listed in `missingOuterIds`, in first-appearance order. The node still cannot pass an existing document off as deleted.

0 commit comments

Comments
 (0)