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
4 changes: 4 additions & 0 deletions .github/package-filters/rs-packages-no-workflows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ dash-context-provider: &context_provider

rs-sdk-trusted-context-provider: &sdk_trusted_context_provider
- packages/rs-sdk-trusted-context-provider/**
# The refresh_checkpoints example's dev-dependencies, anchored further down.
- packages/rs-dapi-client/**
- packages/dapi-grpc/**
- packages/rs-drive-proof-verifier/**
- *dash_async
- *context_provider
- *core_proof
Expand Down
4 changes: 4 additions & 0 deletions .github/package-filters/rs-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ dash-context-provider: &context_provider
rs-sdk-trusted-context-provider: &sdk_trusted_context_provider
- .github/workflows/tests*
- packages/rs-sdk-trusted-context-provider/**
# The refresh_checkpoints example's dev-dependencies, anchored further down.
- packages/rs-dapi-client/**
- packages/dapi-grpc/**
- packages/rs-drive-proof-verifier/**
- *dash_async
- *context_provider
- *core_proof
Expand Down
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions packages/rs-sdk-trusted-context-provider/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ futures = { version = "0.3.30" }
# Monotonic clock that also works on wasm32, for spacing out refreshes.
web-time = "1.1"

[[example]]
name = "refresh_checkpoints"
# Its tests run with the crate's.
test = true

Comment thread
PastaPastaPasta marked this conversation as resolved.
Resolved
[features]
default = []

Expand Down Expand Up @@ -102,6 +107,12 @@ reqwest = { version = "0.12", default-features = false, features = ["json", "rus

[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { version = "1.40", features = ["macros", "rt-multi-thread"] }
# The `refresh_checkpoints` example reads Platform's Core height from proved
# evonode responses.
dapi-grpc = { path = "../dapi-grpc", default-features = false, features = ["platform"] }
dash-network-seeds = { workspace = true }
drive-proof-verifier = { path = "../rs-drive-proof-verifier" }
rs-dapi-client = { path = "../rs-dapi-client" }

[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
# Runs the browser transport's tests under Node.js or a browser.
Expand Down
61 changes: 52 additions & 9 deletions packages/rs-sdk-trusted-context-provider/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,9 @@ response must be is the SDK's policy.

A proof covers every ChainLock quorum rotation between its starting state and
its target, so it grows as the checkpoint ages, and a relay has to keep the
blocks back to it. Each SDK release can move the embedded checkpoints forward.
A long-lived application should persist `latest_verified_state()` and pass it
back to `with_checkpoint` on the next start, so its proofs stay short.
blocks back to it. Each release moves the embedded checkpoints forward, as
below. A long-lived application should persist `latest_verified_state()` and
pass it back to `with_checkpoint` on the next start, so its proofs stay short.

### Limitations

Expand All @@ -256,19 +256,62 @@ back to `with_checkpoint` on the next start, so its proofs stay short.
### Refreshing the embedded checkpoints

`checkpoints/<network>.json` hold Core `ProofState` JSON (hashes in RPC display
order). To move one to a newer block `H`:
order). `yarn release` moves both forward with

```sh
cargo run -p rs-sdk-trusted-context-provider --example refresh_checkpoints -- [mainnet] [testnet]
```
Comment thread
PastaPastaPasta marked this conversation as resolved.
Resolved

For each network it asks the public relay for two proofs from the current
checkpoint, one to the relay's tip and one to a block about a day below it
(just past the oldest active Platform quorum's mining), verifies both, and
writes the lower target as the new checkpoint. Each new checkpoint is proven
from the one it replaces, so a refresh adds no trust in the relay. It also
rewrites `tests/data/checkpoint-evidence-<network>.json` from the proof: the target's
raw header, its coinbase transaction and the coinbase's Merkle branch. The
`checkpoints` test recomputes the block hash, the header's Merkle root and
both roots from that evidence.

`MAINNET_QUORUM_RELAY` or `TESTNET_QUORUM_RELAY` points a network at another
relay serving `/quorums` and `/proofs`, for example a self-hosted one while
public mainnet `/proofs` returns 403 (the provider behind the Platform check
below accepts `http` only on loopback). A network that fails keeps its
checkpoint, and the release prints a warning and records it in the release
PR; an older checkpoint stays valid and only makes proofs longer. Any other
failure, including checkpoint files that cannot be written and an
interrupt, stops the release with the committed checkpoints restored, and
so does a checkpoint that no longer matches its evidence.

The relay's quorum list only suggests the height to ask for. The new
checkpoint must be at least half a Platform quorum lifetime (about 12 hours)
below both the verified tip and the Core height Platform reports, so the
heights Platform reports after the release are above it. A relay cannot prove
a ChainLock above the real tip, and one that hides its tip only makes the
checkpoint older. Platform's Core height stops while Platform halts. It is
read from proved `getEpochsInfo` responses of up to 5 evonodes from the
network's seed list: the height is part of the state a Platform quorum signs,
and this crate's provider proves that quorum's key from the current
checkpoint. Unless a threshold of a Platform quorum signed a false block, the
assumption behind every proof, a verified height is one Platform committed,
and no evonode can report a higher one. The highest verified height is used;
if no evonode gives one, the network keeps its checkpoint.

A refresh relies on the same thing every proof does: that no quorum on the way
from the old checkpoint signed a false ChainLock. A bad checkpoint would carry
into every later SDK build, so the release PR's reviewer should compare each
printed block hash with a block explorer at that height.

To move a checkpoint by hand, for example when no relay can serve it:

1. Pick a height with `chainlock: true` in `getblock` on two independent Core
nodes, and confirm `getblockhash H` and the block hash on a block explorer
agree.
2. Write the state from the block: `network` (0 mainnet, 1 testnet), `height`,
`block_hash`, and the coinbase payload's `merkleRootMNList` and
`merkleRootQuorums` as `masternode_root` and `quorum_root`.
3. Update `tests/data/checkpoint-evidence-<network>.json`: the raw header
(`getblockheader <hash> false`), the raw coinbase transaction, and the
coinbase's Merkle branch (sibling hashes in wire order). The
`checkpoints` test recomputes the block hash, the header's Merkle root and
both roots from this evidence.
3. Write the evidence file: the raw header (`getblockheader <hash> false`), the
raw coinbase transaction, and the coinbase's Merkle branch (sibling hashes
in wire order).

## Implementation Details

Expand Down
Loading
Loading