Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
9de6dac
feat(platform)!: dpns contract v3 checks name registration with schem…
QuantumExplorer Oct 8, 2026
08a5679
fix(platform): a pre-upgrade preorder meets only a one-block minimum age
QuantumExplorer Oct 8, 2026
7fa7b0a
Merge remote-tracking branch 'origin/v5.0-dev' into claude/wizardly-d…
QuantumExplorer Oct 8, 2026
521761b
test(sdk): pin the dpns preorder hash of a mixed-case parent domain name
QuantumExplorer Oct 8, 2026
bdc9d9e
Merge remote-tracking branch 'origin/v5.0-dev' into claude/wizardly-d…
QuantumExplorer Oct 8, 2026
d3518c7
chore(contract): shorten the dpns v3 schema descriptions
QuantumExplorer Oct 9, 2026
d93f498
feat(platform)!: dpns v3 preorders are unique per owner and hash the …
QuantumExplorer Oct 9, 2026
e6c1911
Merge remote-tracking branch 'origin/v5.0-dev' into claude/wizardly-d…
QuantumExplorer Oct 9, 2026
b66668f
fix(sdk): hash DPNS preorders against the contract the network stores
QuantumExplorer Oct 9, 2026
fbd02c9
fix(sdk): take the DPNS contract only from a response proved at the p…
QuantumExplorer Oct 9, 2026
b720af0
fix(drive): seed the block cache with the contract a rebuild stores
QuantumExplorer Oct 9, 2026
51403d3
fix(sdk): skip a DPNS contract response when the SDK version moved me…
QuantumExplorer Oct 9, 2026
4905cf3
Merge remote-tracking branch 'origin/v5.0-dev' into claude/wizardly-d…
QuantumExplorer Oct 9, 2026
1f1a8b0
feat(contract): write the DPNS v3 byte arrays with the type shorthands
QuantumExplorer Oct 9, 2026
45309b9
fix(platform): DPNS v3 review fixes
QuantumExplorer Oct 9, 2026
a9b1ee5
refactor(dpp): read a reference's findBy hash key through one accessor
QuantumExplorer Oct 9, 2026
cec6d0a
perf(drive): delete a consumed document from what its lookup read
QuantumExplorer Oct 9, 2026
0e3ae00
test(sdk): give the records.identity refusal spec two distinct identi…
QuantumExplorer Oct 9, 2026
c2d30d9
docs(book): the findBy function exception for $ownerId; DPNS preorder…
QuantumExplorer Oct 9, 2026
b43fcec
test(drive): run the protocol 13 halves of three tests on DPNS v2
QuantumExplorer Oct 9, 2026
c158a7d
fix(sdk): parse a proof the SDK's version can not read under the vers…
QuantumExplorer Oct 9, 2026
3961808
Merge branch 'v5.0-dev' into claude/wizardly-driscoll-44c981
QuantumExplorer Oct 9, 2026
521bfa2
test(sdk): cover the protocol version fallback through the quorum key…
QuantumExplorer Oct 10, 2026
193c42f
test(sdk): fetch a missing quorum key and read the proof again throug…
QuantumExplorer Oct 10, 2026
35696a2
Merge remote-tracking branch 'origin/v5.0-dev' into claude/wizardly-d…
QuantumExplorer Oct 11, 2026
4be5d8c
Merge remote-tracking branch 'origin/v5.0-dev' into claude/wizardly-d…
QuantumExplorer Oct 11, 2026
759d679
feat(platform)!: dpns v3 preorders hash the owner and the name, witho…
QuantumExplorer Oct 11, 2026
aaa0bb7
fix(sdk): dpns registration decodes under the contract it built again…
QuantumExplorer Oct 11, 2026
b1ae90f
fix(wasm-sdk): the trusted context forwards a registered contract to …
QuantumExplorer Oct 11, 2026
ec975f7
Merge branch 'v5.0-dev' into claude/wizardly-driscoll-44c981
QuantumExplorer Oct 12, 2026
8a35b1a
Merge branch 'v5.0-dev' into claude/wizardly-driscoll-44c981
QuantumExplorer Oct 12, 2026
a7e6bf7
Merge v5.1-dev (with v5.0-dev) into the DPNS v3 branch
QuantumExplorer Oct 13, 2026
1cfe150
feat(platform)!: move DPNS v3 to protocol version 15 and choose the f…
QuantumExplorer Oct 13, 2026
4146766
fix(drive-abci): keep DPNS v3 for its rebuild on an upgrade that skip…
QuantumExplorer Oct 13, 2026
13b2ffb
Merge the DPNS v3 branch, retargeted to protocol version 15
QuantumExplorer Oct 13, 2026
f338eb1
feat(platform)!: delete a consumed document from its lookup's read fr…
QuantumExplorer Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion book/src/contract-keywords/generated-from.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ A client creates `{ "label": "Bob" }` and the stored document holds `{ "label":

The generated property needs no `pattern` of its own. Every value it can hold is what the function generates from params that passed their own patterns: here `label` admits ASCII letters, digits and `-`, so `normalizedLabel` can only ever hold `a` to `z` without `i`, `l` and `o`, digits and `-`. It keeps `maxLength` because it is indexed, and an indexed string declares one of at most 63.

This is the rule the DPNS `domain` type's data trigger checks today for `normalizedLabel` and `normalizedParentDomainName`, written into the schema.
The DPNS `domain` type declares this rule for `normalizedLabel` and `normalizedParentDomainName` from protocol version 15 (DPNS contract v3). Before it, the type's create data trigger checked the same rule.

## Functions

Expand Down
2 changes: 1 addition & 1 deletion book/src/contract-keywords/ownership-and-trading.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Whether an owner may give a document to another identity.

- The owner sends a transfer transition naming the document, the recipient and a `$revision` one higher than the stored one (`InvalidDocumentRevisionError`, 40106). Only the owner may transfer (`DocumentOwnerIdMismatchError`, 40102). The recipient does not have to agree.
- The document gets the recipient as its `$ownerId` and a new `$revision`. A price it was listed at is removed, so a transferred document is no longer for sale. When the type lists `$transferredAt` in `required`, it is set to the block's time, and likewise `$transferredAtBlockHeight` and `$transferredAtCoreBlockHeight` to the block heights.
- A transfer carries no property values, and `$creatorId` keeps naming the identity that created the document. The one change the platform makes itself: from protocol version 13, a DPNS `domain` that is transferred or sold has its `records.identity` pointed at the new owner.
- A transfer carries no property values, and `$creatorId` keeps naming the identity that created the document. The one change the platform makes itself: from protocol version 13, a DPNS `domain` that is transferred or sold has its `records.identity` pointed at the new owner. From protocol version 15 the type's `recordsIdentityIsOwner` rule holds a new name's `records.identity` to its owner until it first changes hands (see [propertyConstraints](property-constraints.md)), so a name can not be transferred or sold in the block that created it.
- The document is checked as it will be stored, with its new owner: against the type's unique indexes (`DuplicateUniqueIndexError`, 40105), against a `distinctFrom: "$ownerId"` property (`DocumentPropertyNotDistinctError`, 10419), and against the `propertyConstraints` rules that read what a transfer changes: `$ownerId`, `$transferredAt` and its heights or `$revision`, of the document or, through `$old.`, of the version before the transfer, or a total that depends on the owner (`DocumentPropertyConstraintViolatedError`, 10422; see [propertyConstraints](property-constraints.md#how-it-works)).
- On a moderated contract, a transfer to a banned or suspended identity is refused (`ContractModerationCounterpartyBarredError`, 41114).
- A transfer of a document past its `ttl` expiry is refused (`DocumentExpiredError`, 40140).
Expand Down
1 change: 1 addition & 0 deletions book/src/contract-keywords/property-constraints.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ A rule can read when the document was created, last updated and last transferred
- `{ "lessThanOrEqual": [{ "subtract": ["endsAt", "$createdAt"] }, 604800000] }` keeps a listing to a week from its creation.
- `{ "lessThanOrEqual": ["$updatedAt", "endsAt"] }` refuses a replace or a price update after the listing ends.
- `{ "lessThanOrEqual": ["$transferredAt", "endsAt"] }` refuses a transfer or a purchase after it ends.
- `{ "ifThen": [{ "equal": ["$transferredAt", "$createdAt"] }, { "equal": ["records.identity", "$ownerId"] }] }` holds `records.identity` to the owner until the document first changes hands. A create records its block's time as the transfer too, so the condition holds on the create, and a transfer or a purchase in a later block records a later time, so it no longer reads the property. One in the block that created the document records that block's time, so the rule still applies to it: a new owner other than `records.identity` is refused. The DPNS `domain` type's `recordsIdentityIsOwner` (protocol version 15) is this rule.

A rule may read one only when the document type records it by listing it in `required`, so every stored document holds it. None takes an `ifAbsent` default, `present` and `absent` refuse them, and an [index-only](index-only.md) type reads none. Each write is judged with the values the stored document ends up with: a create with its block's time and heights for all three events; a replace with the stored creation and transfer values and its block's as the update; a price update with its block's as the update; a transfer or a purchase with its block's as the transfer.

Expand Down
2 changes: 2 additions & 0 deletions book/src/contract-keywords/refers-to-lookup.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ and the `domain`, which can be transferred, declares on its creator:
}
```

DPNS v3 declares this reveal on its `domain` from protocol version 15, its preorders living an hour, with the `where` entry `"$ownerId": "$ownerId"` and `consume`, so the reveal deletes the preorder it found.

`findBy` finds the writer's own preorder of the name, from an earlier block, by the hash of the name, which every preorder of the name holds, whoever made it. `among` names that property: the preorders holding the same value there are the group the found one is chosen among. `validWhen` says when a preorder of the group counts: it holds, at `ownerNameHash`, the hash of its own owner's id and the name, which only an identity knowing the name can compute (`"$ownerId"` there stands for each preorder's owner). `by` orders the group. When the domain is created, the platform checks the found preorder and then reads:

```sql
Expand Down
6 changes: 3 additions & 3 deletions book/src/contract-keywords/transient.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

## Example

The DPNS `domain` type, trimmed to two properties:
The DPNS `domain` type of contracts v1 and v2, trimmed to two properties:

```json
"domain": {
Expand All @@ -36,11 +36,11 @@ The DPNS `domain` type, trimmed to two properties:
}
```

Registering a name takes two steps. A `preorder` document first commits to a hash of the salt and the name, and the `domain` document then reveals both. Every domain create must carry its 32-byte `preorderSalt`, and DPNS checks it against the preorder. Once the domain is created the salt has done its job, and the stored domain does not hold it.
Registering a name took two steps. A `preorder` document first committed to a hash of the salt and the name, and the `domain` document then revealed both. Every domain create carried its 32-byte `preorderSalt`, which the type's create data trigger checked against the preorder. Once the domain was created the salt had done its job, and the stored domain does not hold it. DPNS v3 (protocol version 15) finds the preorder by its creator and the hash of the name instead, choosing the first valid preorder of the name ([Choosing the first commitment](refers-to-lookup.md#choosing-the-first-commitment-choosefirst)), and keeps `preorderSalt` declared, transient and unused, so domains stored under v1 and v2 decode.

## How it works

- **Checked like any other value.** A transient value is on the transition when the document is validated, so the schema holds it to its rules: a required transient property must be present, and its bounds apply. `maxBytes` and `distinctFrom` apply to it too, and so do checks such as the DPNS one above.
- **Checked like any other value.** A transient value is on the transition when the document is validated, so the schema holds it to its rules: a required transient property must be present, and its bounds apply. `maxBytes` and `distinctFrom` apply to it too, and so do checks such as the DPNS v1 and v2 one above.
- **Dropped before storing.** After validation, a create drops the transient values before the document is written. From protocol version 14 a replace drops them the same way. Before 14 a replace stored the values it carried, so a replaced document could hold values its create had dropped.
- **Dropped by top-level name.** Only top-level properties can be transient. When a transient property is an object, the whole object is dropped with everything in it.
- **Never stored, never found.** No stored document holds a transient value, so a query cannot find one and a later reader cannot see it.
Expand Down
249 changes: 249 additions & 0 deletions packages/dpns-contract/schema/v3/dpns-contract-documents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
{
"domain": {
"documentsMutable": false,
"canBeDeleted": false,
"transferable": 1,
"tradeMode": 1,
"keepsTransferHistory": true,
"keepsPurchaseHistory": true,
"keepsPricingHistory": true,
"type": "object",
"indices": [
{
"name": "parentNameAndLabel",
"properties": [
{
"normalizedParentDomainName": "asc"
},
{
"normalizedLabel": "asc"
}
],
"unique": true,
"contested": {
"fieldMatches": [
{
"field": "normalizedLabel",
"regexPattern": "^[a-zA-Z01-]{3,19}$"
}
],
"resolution": 0,
"description": "Normalized labels of 3 to 19 a-z, 0, 1 or - characters go to a masternode vote"
}
},
{
"name": "identityId",
"nullSearchable": false,
"properties": [
{
"records.identity": "asc"
}
]
}
],
"creatorRefersTo": {
"type": "deletableDocument",
"documentType": "preorder",
"findBy": {
"$ownerId": "$ownerId",
"nameHash": {
"function": "sys.hash.sha256d",
"params": [
"normalizedLabel",
{
"const": "."
},
"normalizedParentDomainName"
]
}
},
"where": {
"$ownerId": "$ownerId"
},
"constraints": {
"minimumAgeBlocks": 1,
"chooseFirst": {
"among": "nameHash",
"validWhen": {
"ownerNameHash": {
"function": "sys.hash.sha256d",
"params": [
"$ownerId",
"normalizedLabel",
{
"const": "."
},
"normalizedParentDomainName"
]
}
},
"by": [
"$createdAtBlockHeight"
]
}
},
"consume": true
},
"properties": {
"label": {
"type": "string",
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$",
"minLength": 3,
"maxLength": 63,
"position": 0,
"description": "Label, e.g. 'Bob'"
},
"normalizedLabel": {
"type": "string",
"maxLength": 63,
"generatedFrom": {
"function": "sys.stringTransformations.homographSafeASCII",
"params": [
"label"
]
},
"position": 1,
"description": "Generated: label lowercased, o to 0, i and l to 1, e.g. 'b0b'"
},
"parentDomainName": {
"type": "string",
"pattern": "^$|^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$",
"minLength": 0,
"maxLength": 63,
"position": 2,
"description": "Parent domain name, e.g. 'dash'"
},
"normalizedParentDomainName": {
"type": "string",
"maxLength": 63,
"generatedFrom": {
"function": "sys.stringTransformations.homographSafeASCII",
"params": [
"parentDomainName"
]
},
"position": 3,
"description": "Generated: parentDomainName normalized like normalizedLabel"
},
"preorderSalt": {
"type": "bytes",
"size": 32,
"position": 4,
"description": "Unused from v3; kept so domains written under v1 and v2 decode"
},
"records": {
"type": "object",
"properties": {
"identity": {
"type": "identifier",
"position": 1,
"description": "Identity the name resolves to; its owner's at creation"
}
},
"minProperties": 1,
"position": 5,
"additionalProperties": false
},
"subdomainRules": {
"type": "object",
"properties": {
"allowSubdomains": {
"type": "boolean",
"description": "Anyone may create subdomains (true) or only the owner (false)",
"position": 0
}
},
"position": 6,
"description": "Subdomain rules",
"additionalProperties": false,
"required": [
"allowSubdomains"
]
}
},
"required": [
"$createdAt",
"$updatedAt",
"$transferredAt",
"label",
"normalizedLabel",
"normalizedParentDomainName",
"records",
"subdomainRules"
],
"transient": [
"preorderSalt"
],
"propertyConstraints": {
"recordsIdentityIsOwner": {
"ifThen": [
{
"equal": [
"$transferredAt",
"$createdAt"
]
},
{
"equal": [
"records.identity",
"$ownerId"
]
}
]
}
},
"additionalProperties": false
},
"preorder": {
"documentsMutable": false,
"canBeDeleted": true,
"ttl": 3600,
"type": "object",
"indices": [
{
"name": "ownerAndNameHash",
"properties": [
{
"$ownerId": "asc"
},
{
"nameHash": "asc"
}
],
"unique": true
},
{
"name": "nameHashAndHeight",
"properties": [
{
"nameHash": "asc"
},
{
"$createdAtBlockHeight": "asc"
}
]
}
],
"properties": {
"ownerNameHash": {
"type": "bytes",
"size": 32,
"position": 0,
"description": "sha256d of owner id, normalized label, '.' and normalized parent domain name"
},
"nameHash": {
"type": "bytes",
"size": 32,
"position": 1,
"description": "sha256d of normalized label, '.' and normalized parent domain name"
}
},
"required": [
"$createdAt",
"$createdAtBlockHeight",
"ownerNameHash",
"nameHash"
],
"additionalProperties": false
}
}
8 changes: 5 additions & 3 deletions packages/dpns-contract/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod error;
pub mod v1;
pub mod v2;
pub mod v3;

pub use crate::error::Error;
use platform_value::{Identifier, IdentifierBytes32};
Expand Down Expand Up @@ -28,10 +29,10 @@ pub const ID: Identifier = Identifier(IdentifierBytes32(ID_BYTES));
pub const OWNER_ID: Identifier = Identifier(IdentifierBytes32(OWNER_ID_BYTES));
pub fn load_definitions(platform_version: &PlatformVersion) -> Result<Option<Value>, Error> {
match platform_version.system_data_contracts.dpns {
1 | 2 => Ok(None),
1..=3 => Ok(None),
version => Err(Error::UnknownVersionMismatch {
method: "dpns_contract::load_definitions".to_string(),
known_versions: vec![1, 2],
known_versions: vec![1, 2, 3],
received: version,
}),
}
Expand All @@ -40,9 +41,10 @@ pub fn load_documents_schemas(platform_version: &PlatformVersion) -> Result<Valu
match platform_version.system_data_contracts.dpns {
1 => v1::load_documents_schemas(),
2 => v2::load_documents_schemas(),
3 => v3::load_documents_schemas(),
version => Err(Error::UnknownVersionMismatch {
method: "dpns_contract::load_documents_schemas".to_string(),
known_versions: vec![1, 2],
known_versions: vec![1, 2, 3],
received: version,
}),
}
Expand Down
4 changes: 4 additions & 0 deletions packages/dpns-contract/src/v1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ pub mod document_types {
pub const DASH_ALIAS_IDENTITY_ID: &str = "dashAliasIdentityId";
}
}

pub mod preorder {
pub const NAME: &str = "preorder";
}
}

pub fn load_documents_schemas() -> Result<Value, Error> {
Expand Down
40 changes: 40 additions & 0 deletions packages/dpns-contract/src/v3/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use crate::Error;
use serde_json::Value;

/// The names v3 adds; every other name is v1's ([`crate::v1::document_types`]).
pub mod document_types {
pub mod preorder {
pub mod properties {
/// The double SHA-256 of the owner's id, the normalized label, "." and the
/// normalized parent domain name: a preorder holding it counts (`validWhen`).
pub const OWNER_NAME_HASH: &str = "ownerNameHash";
/// The same hash without the owner's id, shared by every preorder of a name,
/// which the domain's `creatorRefersTo` finds.
pub const NAME_HASH: &str = "nameHash";
}
}
}

/// The document type names are unchanged from v1 (see [`crate::v1::document_types`]),
/// and so are every `domain` property's position and the transient list, so domains
/// written under v1 or v2 decode unchanged: `preorderSalt` stays declared, transient
/// and no longer required, but nothing reads it. v3 replaces most of the `domain`
/// create data trigger with schema keywords: `normalizedLabel` and
/// `normalizedParentDomainName` are generated from their counterparts
/// (`generatedFrom`), `creatorRefersTo` finds the creator's own preorder of the name
/// from an earlier block and deletes it (a `findBy` over the creator and the double
/// SHA-256 of the normalized label, "." and the normalized parent, with
/// `minimumAgeBlocks` and `consume`, which needs the preorder to record
/// `$createdAtBlockHeight`), and refuses it while another identity holds a valid
/// preorder of the name from an earlier block (`chooseFirst` by creation height, a
/// preorder counting when it holds the same hash with its owner's id first), and the
/// `recordsIdentityIsOwner` rule holds a new domain's identity record to its owner. A
/// preorder holds both hashes (`nameHash`, unique per owner and indexed with the
/// creation height, and `ownerNameHash`) and lives an hour (`ttl`). The protocol
/// upgrade deletes the v2 preorders and rebuilds the type. Domains can no longer be
/// deleted (`canBeDeleted: false`).
/// The parent domain checks stay in the create trigger.
pub fn load_documents_schemas() -> Result<Value, Error> {
serde_json::from_str(include_str!("../../schema/v3/dpns-contract-documents.json"))
.map_err(Error::InvalidSchemaJson)
}
Loading
Loading