Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 0 deletions book/src/data-model/documents.md
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,8 @@ Nothing else is verifiable on chain: not that the bytes decrypt, not that they d

In Rust the declaration is `DocumentProperty::encrypted_for` (`Option<EncryptedFor>`), listed per document type by `DocumentTypeV0Getters::encrypted_properties()`, and the shape check is `DocumentTypeBasicMethods::validate_encrypted_property_shapes()`, versioned on the `validate_encrypted_property_shapes` method slot (`None` before protocol version 14, which is what keeps the in-place replace call inert). In JavaScript, `contract.documentTypeEncryptedProperties(name)` and `contract.documentEncryptedProperties` expose the same declarations, and the shape error reaches an app as `DocumentEncryptionErrorCode.InvalidEncryptedPropertyShape`.

Clients encrypt and decrypt through the declaration rather than a per-contract recipe. The Rust SDK's `dash_sdk::platform::encrypted_for` module has `encrypt_property`, which writes the ciphertext and both key id properties, and `decrypt_property`. `EncryptedPropertyEnvelope::read` names the identities and key ids a reader needs. `select_encryption_keys` picks the keys the document type's `identityPublicKey` references demand through their `keyRequirements`. In JavaScript the same helpers are `sdk.encryptedFor.encrypt`, `decrypt` and `envelope` (`WasmSdk.encryptDocumentProperty`, `decryptDocumentProperty` and `encryptedPropertyEnvelope`). The layout has no authentication tag, so a wrong key fails the padding check except about once in 256 attempts, when it yields garbage.

## Rules and Guidelines

**Do:**
Expand Down
26 changes: 26 additions & 0 deletions docs/protocol/moderation-charters.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,29 @@ for the path that seats a team:
| The description is over `SystemLimits::max_moderation_charter_description_length` (4096) bytes; the schema's `maxLength` counts characters | `ModerationCharterDescriptionTooLongError` | 11002 |

`ElectedCharter` reads an elected charter's properties for the same path.

## Reading and writing from a client

Every read is an ordinary proved document query on the system contract, through
the indexes above; no endpoint is specific to charters. The Rust SDK
(`dash_sdk::platform::moderation_charters`) and the JavaScript SDK
(`sdk.moderationCharters` in `@dashevo/evo-sdk`) offer them by name:

| Read | Query | Rust | JavaScript |
| --- | --- | --- | --- |
| A contract's seated charter | `electedCharter.byTargetContract`, at most one | `Sdk::fetch_seated_charter` | `seatedCharter` |
| A proposal | `submittedCharter` by id | `Sdk::fetch_submitted_charter` | `submittedCharter` |
| The team | the seated charter, then `addedModerator` and `removedModerator` by `byElectedCharterMember`, combined as `ElectedCharter::active_members` does | `Sdk::fetch_moderation_team` | `team` |
| The proposals for a contract | `submittedCharter.byTargetContract`, in filing order, paged | `Sdk::fetch_submitted_charters` | `submittedCharters` |
| The join requests for a proposal | `joinRequest.bySubmittedCharter`, paged | `Sdk::fetch_join_requests` | `joinRequests` |
| A charter's pending resignation requests | `resignationRequest.byElectedCharterOwner`, less the writers the charter has a `removedModerator` for | `Sdk::fetch_pending_resignation_requests` | `pendingResignationRequests` |

`Sdk::build_join_request` and `Sdk::build_resignation_request`
(`buildJoinRequest` and `buildResignationRequest` in JavaScript) build the two
documents whose message only the leader reads. They pick the keys the schema's
`keyRequirements` demand, the leader's decryption key bound to
`submittedCharter` and the writer's encryption key bound to `joinRequest`,
encrypt the message and set `recipientId`, `recipientKeyId` and `senderKeyId`.
The encryption is the generic `encryptedFor` helper
(`dash_sdk::platform::encrypted_for`, `sdk.encryptedFor`), which reads the
declaration from any contract; the leader decrypts with it too.
78 changes: 77 additions & 1 deletion packages/js-evo-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ The SDK organises its API into domain-specific facades, each accessible as a pro
| [`sdk.group`](src/group/facade.ts) | Group membership, actions, and contested resources |
| [`sdk.voting`](src/voting/facade.ts) | Contested resource vote states and polls |
| [`sdk.shielded`](src/shielded/facade.ts) | Query shielded pool state, encrypted notes, anchors, and nullifier status |
| [`sdk.encryptedFor`](src/encrypted-for/facade.ts) | Encrypt and decrypt the byte properties a document type declares `encryptedFor`, for any contract |
| [`sdk.moderationCharters`](src/moderation-charters/facade.ts) | Read a contract's seated charter, its team, proposals and join requests; build join and resignation requests |

A `wallet` namespace is also exported with utilities for BIP39 mnemonic generation and validation, BIP44/DIP9/DIP13 key derivation (path helpers included), extended-key conversion (`xprvToXpub`, `deriveChildPublicKey`), key-pair generation and import (`generateKeyPair`, `keyPairFromWif`, `keyPairFromHex`), public-key-to-address conversion, address validation, message signing, and Dashpay contact-key derivation. See [`src/wallet/functions.ts`](src/wallet/functions.ts) for the full list.

Expand Down Expand Up @@ -286,7 +288,81 @@ try {
}
```

Encrypt and decrypt helpers keyed off the declaration are not part of the SDK yet; the Rust `platform-encryption` crate has the primitives.
`sdk.encryptedFor` encrypts and decrypts such a property, reading the declaration from the contract, so the same calls work for every contract that declares one. They run locally and need no connection.

```ts
import { PrivateKey } from '@dashevo/evo-sdk';

// The writer: the fields to set on the document, the ciphertext and both key ids
const fields = await sdk.encryptedFor.encrypt({
dataContract: contract,
documentTypeName: 'joinRequest',
property: 'encryptedMessage',
plaintext: 'I would like to help moderate',
senderKey: writerIdentity.getPublicKeyById(4), // its id goes into senderKeyId
senderPrivateKey: PrivateKey.fromWIF(writerKeyWif),
recipientKey: leaderIdentity.getPublicKeyById(2), // its id goes into recipientKeyId
});
// { encryptedMessage: Uint8Array(48), recipientKeyId: 2, senderKeyId: 4 }

// The reader: whose keys the stored document names, then decrypt
const envelope = await sdk.encryptedFor.envelope({ dataContract: contract, document, property: 'encryptedMessage' });
const sender = await sdk.identities.fetch(envelope.senderId);
const message = await sdk.encryptedFor.decrypt({
dataContract: contract,
document,
property: 'encryptedMessage',
recipientPrivateKey: PrivateKey.fromWIF(leaderDecryptionKeyWif), // the key recipientKeyId names
senderKey: sender.getPublicKeyById(envelope.senderKeyId),
});
```

The IV is fresh randomness on every call. The scheme carries no authentication tag: a wrong key is caught only by the padding check, which it passes about once in 256 attempts and then returns garbage, so an app that must tell the two apart has to recognise its plaintext. ECDH is symmetric, so the writer can read its own message back with its private key and the recipient's key.

## Moderation charters

A contract that declares elected moderation is moderated by the team of its seated charter in the moderation charters system contract (protocol version 14, `EG7RGfV8fDTayC2FyVr8HwdpJh3fXDbVztcfE94UmN88`). `sdk.moderationCharters` reads it with ordinary proved document queries:

```ts
// The seated charter: the one electedCharter for the contract, or undefined
const charter = await sdk.moderationCharters.seatedCharter(contractId);

// Its proposal, the submittedCharter it runs on
const proposal = await sdk.moderationCharters.submittedCharter(charter.properties.submittedCharterId);

// The team: the leader plus the elected members and the additions, less the removals
const team = await sdk.moderationCharters.team(contractId);
team.leaderId; team.members; team.contains(identityId);

// Proposals for a contract in filing order, and the join requests for one, a page at a time
const proposals = await sdk.moderationCharters.submittedCharters({ targetContractId: contractId, limit: 20 });
const requests = await sdk.moderationCharters.joinRequests({ submittedCharterId: proposalId });

// Resignation requests the leader has not acted on with a removal yet
const pending = await sdk.moderationCharters.pendingResignationRequests(charter.id);
```

A join request and a resignation request carry a message only the leader can read. The builders fetch the proposal (or the charter) and the leader, pick the leader's decryption key bound to `submittedCharter` and the writer's encryption key bound to `joinRequest`, the keys the schema's `keyRequirements` demand, encrypt the message and set `recipientId`, `recipientKeyId` and `senderKeyId`:

```ts
const joinRequest = await sdk.moderationCharters.buildJoinRequest({
submittedCharterId: proposalId,
message: 'Five years moderating a forum; happy to help',
writer: identity, // or its id
writerEncryptionKey: PrivateKey.fromWIF(encryptionKeyWif),
});
await sdk.documents.create({ document: joinRequest, identityKey, signer });

const resignation = await sdk.moderationCharters.buildResignationRequest({
electedCharterId: charter.id,
message: 'Stepping down at the end of the month',
writer: identity,
writerEncryptionKey: PrivateKey.fromWIF(encryptionKeyWif),
});
await sdk.documents.create({ document: resignation, identityKey, signer });
```

The leader reads either with `sdk.encryptedFor.decrypt`.

## Immutable properties (`immutable`)

Expand Down
47 changes: 47 additions & 0 deletions packages/js-evo-sdk/src/encrypted-for/facade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as wasm from '../wasm.js';
import type { EvoSDK } from '../sdk.js';

/**
* Encrypting and decrypting the byte properties a document type declares `encryptedFor`.
*
* Every method reads the declaration from the contract given, so it works for any contract
* that declares one. None needs a connection: they run locally.
*/
export class EncryptedForFacade {
private sdk: EvoSDK;

constructor(sdk: EvoSDK) {
this.sdk = sdk;
}

/**
* Encrypts a message into a property declaring `encryptedFor`.
*
* @returns The properties to set on the document: the ciphertext at `property` and the two
* key ids at the declaration's `recipientKey` and `senderKey` paths. The recipient property
* is the caller's to set.
*/
async encrypt(options: wasm.EncryptDocumentPropertyOptions): Promise<Record<string, unknown>> {
await wasm.ensureInitialized();
return wasm.WasmSdk.encryptDocumentProperty(options);
}

/**
* Decrypts a property declaring `encryptedFor`. The scheme carries no authentication tag:
* a wrong key is caught only by the padding check, which it passes about once in 256
* attempts, returning garbage.
*/
async decrypt(options: wasm.DecryptDocumentPropertyOptions): Promise<Uint8Array> {
await wasm.ensureInitialized();
return wasm.WasmSdk.decryptDocumentProperty(options);
}

/**
* Whose keys an encrypted property of a document is under: the recipient and sender
* identities and the ids of their keys, which a reader fetches to decrypt it.
*/
async envelope(options: wasm.EncryptedPropertyEnvelopeOptions): Promise<wasm.EncryptedPropertyEnvelope> {
await wasm.ensureInitialized();
return wasm.WasmSdk.encryptedPropertyEnvelope(options);
}
}
80 changes: 80 additions & 0 deletions packages/js-evo-sdk/src/moderation-charters/facade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as wasm from '../wasm.js';
import type { EvoSDK } from '../sdk.js';

/**
* The moderation charters system contract (protocol version 14): who moderates a contract
* that declares elected moderation, the proposals and join requests behind it, and the
* requests members send the leader. Every read is a proved document query.
*/
export class ModerationChartersFacade {
private sdk: EvoSDK;

constructor(sdk: EvoSDK) {
this.sdk = sdk;
}

/**
* The seated charter of a contract: its `electedCharter`, or undefined when it has none.
* Only a contest's winner is ever stored, so there is at most one.
*/
async seatedCharter(targetContractId: wasm.IdentifierLike): Promise<wasm.Document | undefined> {
const w = await this.sdk.getWasmSdkConnected();
return w.getModerationSeatedCharter(targetContractId);
}

/** A proposal (`submittedCharter`) by id, such as a seated charter's `submittedCharterId`. */
async submittedCharter(submittedCharterId: wasm.IdentifierLike): Promise<wasm.Document | undefined> {
const w = await this.sdk.getWasmSdkConnected();
return w.getModerationSubmittedCharter(submittedCharterId);
}

/**
* The team that moderates a contract: the seated charter's leader plus its elected members
* and the members the leader added, less those the leader removed. Undefined when the
* contract has no seated charter.
*/
async team(targetContractId: wasm.IdentifierLike): Promise<wasm.ModerationTeam | undefined> {
const w = await this.sdk.getWasmSdkConnected();
return w.getModerationTeam(targetContractId);
}

/** One page of the proposals for a contract, in filing order. */
async submittedCharters(
query: wasm.ModerationSubmittedChartersQuery,
): Promise<Map<string, wasm.Document | undefined>> {
const w = await this.sdk.getWasmSdkConnected();
return w.getModerationSubmittedCharters(query);
}

/** One page of the join requests for a proposal, in the order of their owners' ids. */
async joinRequests(
query: wasm.ModerationJoinRequestsQuery,
): Promise<Map<string, wasm.Document | undefined>> {
const w = await this.sdk.getWasmSdkConnected();
return w.getModerationJoinRequests(query);
}

/** The resignation requests for a seated charter that the leader has not acted on. */
async pendingResignationRequests(electedCharterId: wasm.IdentifierLike): Promise<wasm.Document[]> {
const w = await this.sdk.getWasmSdkConnected();
return w.getModerationPendingResignationRequests(electedCharterId);
}

/**
* Builds a join request whose message only the proposal's leader can read. Pass the result
* to `documents.create`.
*/
async buildJoinRequest(options: wasm.ModerationJoinRequestOptions): Promise<wasm.Document> {
const w = await this.sdk.getWasmSdkConnected();
return w.buildModerationJoinRequest(options);
}

/**
* Builds a resignation request whose message only the leader can read. Pass the result to
* `documents.create`; deleting it withdraws the request.
*/
async buildResignationRequest(options: wasm.ModerationResignationRequestOptions): Promise<wasm.Document> {
const w = await this.sdk.getWasmSdkConnected();
return w.buildModerationResignationRequest(options);
}
}
8 changes: 8 additions & 0 deletions packages/js-evo-sdk/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { GroupFacade } from './group/facade.js';
import { ContractGroupsFacade } from './contract-groups/facade.js';
import { VotingFacade } from './voting/facade.js';
import { ShieldedFacade } from './shielded/facade.js';
import { EncryptedForFacade } from './encrypted-for/facade.js';
import { ModerationChartersFacade } from './moderation-charters/facade.js';

export interface ConnectionOptions {
version?: number;
Expand Down Expand Up @@ -73,6 +75,8 @@ export class EvoSDK {
public contractGroups!: ContractGroupsFacade;
public voting!: VotingFacade;
public shielded!: ShieldedFacade;
public encryptedFor!: EncryptedForFacade;
public moderationCharters!: ModerationChartersFacade;
constructor(options: EvoSDKOptions = {}) {
// Apply defaults while preserving any future connection options
const { network = 'testnet', trusted = false, addresses, devnetName, quorumUrl, ...connection } = options;
Expand Down Expand Up @@ -111,6 +115,8 @@ export class EvoSDK {
this.contractGroups = new ContractGroupsFacade(this);
this.voting = new VotingFacade(this);
this.shielded = new ShieldedFacade(this);
this.encryptedFor = new EncryptedForFacade(this);
this.moderationCharters = new ModerationChartersFacade(this);
}

get wasm(): wasm.WasmSdk {
Expand Down Expand Up @@ -335,5 +341,7 @@ export { GroupFacade } from './group/facade.js';
export { ContractGroupsFacade } from './contract-groups/facade.js';
export { VotingFacade } from './voting/facade.js';
export { ShieldedFacade } from './shielded/facade.js';
export { EncryptedForFacade } from './encrypted-for/facade.js';
export { ModerationChartersFacade } from './moderation-charters/facade.js';
export { wallet } from './wallet/functions.js';
export * from './wasm.js';
Loading
Loading