feat(bootstrap): RPC-driven trusted snapshot publisher rotation (v6.1.8) (#26)
Design A: single-slot runtime override via RPC. The previous publisher is dropped atomically on every set. The built-in fallback list (TG8f76yktTxDrT7JJymY3wVAusXiD3fVvX, Sami's legacy key) is always consulted if no runtime override is set, so a fresh daemon still verifies old snapshots without operator intervention. New RPCs: - settrustedv2snapshotpublisher <address> - gettrustedv2snapshotpublisher - unsettrustedv2snapshotpublisher Persistence: <datadir>/snapshot-publisher.json (plain JSON). Loaded at startup in init.cpp before any snapshot verification. Files: src/bootstrap.cpp (+116 / -8) Replace hardcoded list with single-slot + fallback src/bootstrap.h (+21) Declare new Bootstrap:: functions src/init.cpp (+3) LoadTrustedSnapshotPublisher() at startup src/rpcblockchain.cpp (+89) Three new RPC function bodies src/rpcblockchain.cpp (+1) #include "bootstrap.h" src/trianglesrpc.cpp (+3) Register three new commands src/trianglesrpc.h (+3) extern declarations README.md (+30) New 'Trusted Snapshot Publisher' sections TRIANGLES-RPC-COMMANDS.md (+3) Three new rows in Blockchain table docs/snapshot-publisher.md (new, +240) Full operator handoff guide Co-authored-by: Krystie <krystie@openclaw.local>
This commit is contained in:
@@ -175,6 +175,29 @@ Check staking status:
|
|||||||
trianglesd getstakinginfo
|
trianglesd getstakinginfo
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Trusted Snapshot Publisher (UTXO Snapshots)
|
||||||
|
|
||||||
|
The daemon verifies that any UTXO snapshot it loads was signed by a
|
||||||
|
**trusted publisher**. Starting with v6.1.8, the trusted publisher can
|
||||||
|
be rotated at runtime via RPC — no rebuild required. The compiled-in
|
||||||
|
fallback (`TG8f76yktTxDrT7JJymY3wVAusXiD3fVvX`, Sami's legacy key)
|
||||||
|
remains in effect if no runtime override is set.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Rotate to a new publisher
|
||||||
|
trianglesd settrustedv2snapshotpublisher TGotWuftzH7rD9tXC7whE8EXiyC3mr1CrH
|
||||||
|
|
||||||
|
# Check current publisher
|
||||||
|
trianglesd gettrustedv2snapshotpublisher
|
||||||
|
|
||||||
|
# Revert to the compiled-in fallback
|
||||||
|
trianglesd unsettrustedv2snapshotpublisher
|
||||||
|
```
|
||||||
|
|
||||||
|
The model is single-slot: calling `settrustedv2snapshotpublisher`
|
||||||
|
atomically drops the previous publisher. See `docs/snapshot-publisher.md`
|
||||||
|
for the full operator guide.
|
||||||
|
|
||||||
### Encrypted Messaging
|
### Encrypted Messaging
|
||||||
|
|
||||||
Send and receive encrypted messages between wallet addresses:
|
Send and receive encrypted messages between wallet addresses:
|
||||||
@@ -234,6 +257,13 @@ Then set `externalip=<your-onion-address>` in `triangles.conf`.
|
|||||||
- `smsglocalkeys` - List messaging-enabled addresses
|
- `smsglocalkeys` - List messaging-enabled addresses
|
||||||
- `smsgscanchain` - Scan blockchain for public keys
|
- `smsgscanchain` - Scan blockchain for public keys
|
||||||
|
|
||||||
|
### Trusted Snapshot Publisher (v6.1.8+)
|
||||||
|
- `settrustedv2snapshotpublisher <address>` - Atomically replace the trusted snapshot publisher (previous one dropped immediately). Persists to `<datadir>/snapshot-publisher.json`.
|
||||||
|
- `gettrustedv2snapshotpublisher` - Returns the currently active runtime publisher and whether a runtime override is in effect.
|
||||||
|
- `unsettrustedv2snapshotpublisher` - Clear the runtime override and revert to the compiled-in fallback list.
|
||||||
|
|
||||||
|
See `docs/snapshot-publisher.md` for the full operator guide.
|
||||||
|
|
||||||
## Chain History
|
## Chain History
|
||||||
|
|
||||||
- **July 16, 2014** - Genesis block
|
- **July 16, 2014** - Genesis block
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ Triangles is a Tor-only PoS cryptocurrency. PoW ended at block 9000; from block
|
|||||||
| `getrawmempool` | | Returns all transaction IDs currently in the mempool. |
|
| `getrawmempool` | | Returns all transaction IDs currently in the mempool. |
|
||||||
| `getcheckpoint` | | Returns info about the current synchronized checkpoint. |
|
| `getcheckpoint` | | Returns info about the current synchronized checkpoint. |
|
||||||
| `getchaintips` | | Returns info about all known chain tips (forks). |
|
| `getchaintips` | | Returns info about all known chain tips (forks). |
|
||||||
|
| `settrustedv2snapshotpublisher` | `<address>` | Atomically replaces the trusted snapshot publisher. The previous publisher is dropped immediately (no grace period). The new publisher is persisted to `<datadir>/snapshot-publisher.json`. Returns `{ previous, current }`. See `docs/snapshot-publisher.md`. |
|
||||||
|
| `gettrustedv2snapshotpublisher` | | Returns the currently active trusted snapshot publisher and whether a runtime override is in effect. Returns `{ active, has_runtime_override }`. |
|
||||||
|
| `unsettrustedv2snapshotpublisher` | | Clears the runtime trusted snapshot publisher override. Reverts to the built-in fallback list (compiled in). Removes `<datadir>/snapshot-publisher.json`. |
|
||||||
| `invalidateblock` | `<hash>` | Permanently marks a block as invalid and rewinds the chain past it. |
|
| `invalidateblock` | `<hash>` | Permanently marks a block as invalid and rewinds the chain past it. |
|
||||||
| `reconsiderblock` | `<hash>` | Removes the invalid mark from a previously invalidated block. |
|
| `reconsiderblock` | `<hash>` | Removes the invalid mark from a previously invalidated block. |
|
||||||
| `recalculatesupply` | | Recalculates money supply by summing all UTXOs. Updates the stored value at the chain tip and persists to disk. Returns old/new supply and difference. |
|
| `recalculatesupply` | | Recalculates money supply by summing all UTXOs. Updates the stored value at the chain tip and persists to disk. Returns old/new supply and difference. |
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
# Trusted Snapshot Publisher — Operator Guide
|
||||||
|
|
||||||
|
This document explains how the trusted snapshot publisher mechanism works
|
||||||
|
in Triangles and how to rotate the publisher without rebuilding the
|
||||||
|
daemon. It is written for the person who operates the Triangles network
|
||||||
|
after Sami — whoever that turns out to be.
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
The Triangles daemon verifies that any UTXO snapshot it loads was
|
||||||
|
**signed by a trusted publisher**. This prevents a malicious snapshot
|
||||||
|
file from tricking a node into accepting a fake chain state.
|
||||||
|
|
||||||
|
In versions before v6.1.8, the trusted publisher list was hardcoded
|
||||||
|
in the binary. To rotate keys, the daemon had to be rebuilt and
|
||||||
|
re-released. That was bad for handover.
|
||||||
|
|
||||||
|
Starting with v6.1.8, the daemon supports a **runtime-configurable
|
||||||
|
single-slot trusted publisher** via RPC. The compiled-in fallback list
|
||||||
|
is still consulted if no runtime publisher is set, so a fresh daemon
|
||||||
|
never fails to verify an old snapshot.
|
||||||
|
|
||||||
|
## The model — Design A (single-slot, auto-replace)
|
||||||
|
|
||||||
|
- **At most ONE runtime publisher exists at any time.**
|
||||||
|
- Calling `settrustedv2snapshotpublisher <addr>` **atomically
|
||||||
|
replaces** the current publisher. The previous one is dropped
|
||||||
|
immediately. There is no grace period, no retirement list, no
|
||||||
|
rollback path. Pure single-slot.
|
||||||
|
- The active publisher is persisted to
|
||||||
|
`<datadir>/snapshot-publisher.json`, so it survives daemon
|
||||||
|
restarts.
|
||||||
|
- The built-in fallback list (read-only, compiled into the binary) is
|
||||||
|
consulted only if no runtime publisher is set. That list contains:
|
||||||
|
- `TG8f76yktTxDrT7JJymY3wVAusXiD3fVvX` — Sami's legacy snapshot
|
||||||
|
publisher key (the original, used from v6.1.5 through v6.1.7).
|
||||||
|
|
||||||
|
## The three RPCs
|
||||||
|
|
||||||
|
### `settrustedv2snapshotpublisher <address>`
|
||||||
|
|
||||||
|
Atomically replaces the active trusted publisher. The previous
|
||||||
|
publisher is dropped immediately. The new publisher is persisted to
|
||||||
|
`<datadir>/snapshot-publisher.json` so the choice survives restarts.
|
||||||
|
|
||||||
|
```
|
||||||
|
triangles-cli settrustedv2snapshotpublisher TGotWuftzH7rD9tXC7whE8EXiyC3mr1CrH
|
||||||
|
```
|
||||||
|
|
||||||
|
Result:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"previous": "TG8f76yktTxDrT7JJymY3wVAusXiD3fVvX",
|
||||||
|
"current": "TGotWuftzH7rD9tXC7whE8EXiyC3mr1CrH"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `previous` field is empty if no runtime publisher was set before.
|
||||||
|
|
||||||
|
### `gettrustedv2snapshotpublisher`
|
||||||
|
|
||||||
|
Returns the currently active runtime publisher.
|
||||||
|
|
||||||
|
```
|
||||||
|
triangles-cli gettrustedv2snapshotpublisher
|
||||||
|
```
|
||||||
|
|
||||||
|
Result:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"active": "TGotWuftzH7rD9tXC7whE8EXiyC3mr1CrH",
|
||||||
|
"has_runtime_override": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If `has_runtime_override` is `false`, only the built-in fallback list
|
||||||
|
is consulted. The fallback currently contains `TG8f76yktTxDrT7JJymY3wVAusXiD3fVvX`.
|
||||||
|
|
||||||
|
### `unsettrustedv2snapshotpublisher`
|
||||||
|
|
||||||
|
Clears the runtime override. Reverts to the built-in fallback list.
|
||||||
|
Also removes `<datadir>/snapshot-publisher.json`.
|
||||||
|
|
||||||
|
```
|
||||||
|
triangles-cli unsettrustedv2snapshotpublisher
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this if you want to "go back to the legacy trusted signer"
|
||||||
|
without a rebuild.
|
||||||
|
|
||||||
|
## Common rotation scenarios
|
||||||
|
|
||||||
|
### Rotate to a new key (forward rotation)
|
||||||
|
|
||||||
|
1. Generate a new key in the wallet:
|
||||||
|
```
|
||||||
|
triangles-cli getnewaddress
|
||||||
|
# returns: TNewAddressHere...
|
||||||
|
```
|
||||||
|
2. (Optional but recommended) Label it so you remember its role:
|
||||||
|
```
|
||||||
|
triangles-cli setaccount TNewAddressHere... "snapshot publisher"
|
||||||
|
```
|
||||||
|
3. Set it as the trusted publisher:
|
||||||
|
```
|
||||||
|
triangles-cli settrustedv2snapshotpublisher TNewAddressHere...
|
||||||
|
```
|
||||||
|
4. Verify:
|
||||||
|
```
|
||||||
|
triangles-cli gettrustedv2snapshotpublisher
|
||||||
|
```
|
||||||
|
Should show `active: TNewAddressHere...`.
|
||||||
|
|
||||||
|
Old publisher is dropped immediately. New one is in effect for this
|
||||||
|
daemon and any daemon that syncs from `<datadir>/snapshot-publisher.json`.
|
||||||
|
|
||||||
|
### Roll back to the legacy publisher
|
||||||
|
|
||||||
|
If the new key is lost / compromised / you just want to revert:
|
||||||
|
|
||||||
|
```
|
||||||
|
triangles-cli unsettrustedv2snapshotpublisher
|
||||||
|
```
|
||||||
|
|
||||||
|
This reverts to the built-in fallback (`TG8f76ykt...`). No rebuild
|
||||||
|
required. The legacy address will continue to verify any snapshot
|
||||||
|
that was signed before your rotation.
|
||||||
|
|
||||||
|
### Rotate during a handover (publisher A hands off to publisher B)
|
||||||
|
|
||||||
|
1. Publisher B installs v6.1.8+ daemon.
|
||||||
|
2. Publisher B sets themselves as the trusted publisher:
|
||||||
|
```
|
||||||
|
triangles-cli settrustedv2snapshotpublisher TBsAddress...
|
||||||
|
```
|
||||||
|
3. Publisher B signs a new snapshot with their key (see
|
||||||
|
`publishcheckpoint` in `TRIANGLES-RPC-COMMANDS.md`).
|
||||||
|
4. Publisher A can leave the network; their key is no longer trusted
|
||||||
|
on any node that has called `settrustedv2snapshotpublisher`.
|
||||||
|
|
||||||
|
Note: because Design A auto-drops the previous publisher, **only one
|
||||||
|
operator can publish at a time.** If you need overlap (both A and B
|
||||||
|
publishing during a transition), that requires Design B (multi-slot
|
||||||
|
with grace period) — not supported in v6.1.8. Contact Sami for the
|
||||||
|
upgrade path.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `<datadir>/snapshot-publisher.json` | Runtime publisher override. Plain JSON. Inspectable with `cat`. |
|
||||||
|
| `<datadir>/wallet.dat` | Must contain the privkey for the active publisher, otherwise `publishcheckpoint` will fail at signing time. (Trust is governed by the override; signing is governed by the wallet.) |
|
||||||
|
|
||||||
|
### `<datadir>/snapshot-publisher.json` format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"address": "TGotWuftzH7rD9tXC7whE8EXiyC3mr1CrH",
|
||||||
|
"set_at": 1752168000,
|
||||||
|
"note": "Set via triangles-cli settrustedv2snapshotpublisher. Replace atomically; previous publisher is dropped."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`set_at` is the Unix timestamp when the RPC was last called. `note` is
|
||||||
|
informational only.
|
||||||
|
|
||||||
|
## Recovery if RPC fails
|
||||||
|
|
||||||
|
If for some reason the runtime override can't be persisted (e.g. JSON
|
||||||
|
write fails), the RPC returns a warning but the in-memory change is
|
||||||
|
already live for the current session. To check:
|
||||||
|
|
||||||
|
```
|
||||||
|
triangles-cli gettrustedv2snapshotpublisher
|
||||||
|
```
|
||||||
|
|
||||||
|
If `active` is set, you're good for the current session. The next
|
||||||
|
daemon restart will lose it unless `snapshot-publisher.json` exists.
|
||||||
|
Inspect it manually:
|
||||||
|
|
||||||
|
```
|
||||||
|
cat ~/.triangles/snapshot-publisher.json
|
||||||
|
```
|
||||||
|
|
||||||
|
If the file doesn't exist but you need the override to survive restart,
|
||||||
|
hand-write it:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"address": "TGotWuftzH7rD9tXC7whE8EXiyC3mr1CrH",
|
||||||
|
"set_at": 1752168000,
|
||||||
|
"note": "Hand-set; rotate via triangles-cli settrustedv2snapshotpublisher."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The daemon reads this file at startup. Address must be 34 chars and
|
||||||
|
start with `T`. Anything else is logged and ignored.
|
||||||
|
|
||||||
|
## When you DO need a rebuild
|
||||||
|
|
||||||
|
- **Adding a new entry to the built-in fallback list** (the
|
||||||
|
read-only list compiled into the binary). Edit
|
||||||
|
`BUILTIN_TRUSTED_SNAPSHOT_SIGNERS[]` in `src/bootstrap.cpp`, rebuild,
|
||||||
|
release. This is only needed if you want a publisher to be trusted
|
||||||
|
*without* any operator running the RPC.
|
||||||
|
- **Changing the RPC names or argument shapes.** Edit source, rebuild.
|
||||||
|
|
||||||
|
For everyday "I want to add or rotate a trusted publisher," the RPC
|
||||||
|
is enough. Don't rebuild.
|
||||||
|
|
||||||
|
## Why "single-slot, no grace period"
|
||||||
|
|
||||||
|
Sami asked for it explicitly when designing the operator-experience
|
||||||
|
for this feature. The trade-off: if the active key is lost or
|
||||||
|
compromised, there's no automatic fallback. The operator must either
|
||||||
|
re-add the previous key (which requires they kept the JSON file or
|
||||||
|
remember the address) or rebuild with the new key in
|
||||||
|
`BUILTIN_TRUSTED_SNAPSHOT_SIGNERS[]`.
|
||||||
|
|
||||||
|
If this trade-off becomes painful — for example if multiple
|
||||||
|
operators need to publish during a handover — the alternative is
|
||||||
|
Design B (multi-slot with grace period). That's a one-day patch on
|
||||||
|
top of this one. Ask Sami for the upgrade.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
This feature is introduced in **v6.1.8**. Daemons older than v6.1.8
|
||||||
|
still use the hardcoded `TG8f76ykt...` only — they cannot use the new
|
||||||
|
key until they upgrade.
|
||||||
|
|
||||||
|
## Related RPCs
|
||||||
|
|
||||||
|
For the publishing side (signing snapshots, not verifying them),
|
||||||
|
see:
|
||||||
|
|
||||||
|
- `publishcheckpoint <interval> <signing_address> <output_path>` —
|
||||||
|
builds and signs a checkpoint document.
|
||||||
|
- `gencheckpoints` — generates raw checkpoint data without signing.
|
||||||
|
- `getcheckpoint` — returns the current synchronized checkpoint.
|
||||||
|
|
||||||
|
See `TRIANGLES-RPC-COMMANDS.md` for full details on those.
|
||||||
+118
-6
@@ -775,16 +775,128 @@ namespace {
|
|||||||
// Trusted signer addresses for snapshot manifests. A snapshot is accepted
|
// Trusted signer addresses for snapshot manifests. A snapshot is accepted
|
||||||
// iff its manifest's signing_address matches one of these AND its signature
|
// iff its manifest's signing_address matches one of these AND its signature
|
||||||
// verifies under Triangles' compact-message protocol.
|
// verifies under Triangles' compact-message protocol.
|
||||||
static const char* TRUSTED_SNAPSHOT_SIGNERS[] = {
|
//
|
||||||
"TG8f76yktTxDrT7JJymY3wVAusXiD3fVvX", // Sami's snapshot publisher key
|
// Design A: single-slot runtime override via RPC. The previous publisher
|
||||||
|
// is dropped atomically on every set. The built-in fallback below is
|
||||||
|
// always consulted if no runtime override is set, so a fresh daemon still
|
||||||
|
// verifies old snapshots without operator intervention.
|
||||||
|
|
||||||
|
// Built-in fallback (read-only, compiled in).
|
||||||
|
static const char* BUILTIN_TRUSTED_SNAPSHOT_SIGNERS[] = {
|
||||||
|
"TG8f76yktTxDrT7JJymY3wVAusXiD3fVvX", // Sami's legacy snapshot publisher key
|
||||||
};
|
};
|
||||||
static const size_t NUM_TRUSTED_SNAPSHOT_SIGNERS =
|
static const size_t NUM_BUILTIN_TRUSTED_SNAPSHOT_SIGNERS =
|
||||||
sizeof(TRUSTED_SNAPSHOT_SIGNERS) / sizeof(TRUSTED_SNAPSHOT_SIGNERS[0]);
|
sizeof(BUILTIN_TRUSTED_SNAPSHOT_SIGNERS) / sizeof(BUILTIN_TRUSTED_SNAPSHOT_SIGNERS[0]);
|
||||||
|
|
||||||
|
// Runtime override. Empty string = no override, use built-in fallback.
|
||||||
|
static std::string g_activeTrustedSnapshotPublisher;
|
||||||
|
static std::mutex g_trustedPublisherMutex;
|
||||||
|
static const char* SNAPSHOT_PUBLISHER_FILE = "snapshot-publisher.json";
|
||||||
|
|
||||||
|
std::string GetActiveTrustedSnapshotPublisher()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_trustedPublisherMutex);
|
||||||
|
return g_activeTrustedSnapshotPublisher;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SetActiveTrustedSnapshotPublisherUnlocked(const std::string& addr)
|
||||||
|
{
|
||||||
|
g_activeTrustedSnapshotPublisher = addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load runtime override from <datadir>/snapshot-publisher.json.
|
||||||
|
// Called once at startup from init.cpp.
|
||||||
|
void LoadTrustedSnapshotPublisher()
|
||||||
|
{
|
||||||
|
fs::path filePath = GetDataDir(true) / SNAPSHOT_PUBLISHER_FILE;
|
||||||
|
if (!fs::exists(filePath))
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::ifstream f(filePath.string().c_str());
|
||||||
|
if (!f) return;
|
||||||
|
|
||||||
|
std::stringstream ss; ss << f.rdbuf();
|
||||||
|
std::string json = ss.str();
|
||||||
|
|
||||||
|
// Minimal JSON parse: "address":"<addr>"
|
||||||
|
size_t keyPos = json.find("\"address\"");
|
||||||
|
if (keyPos == std::string::npos) return;
|
||||||
|
size_t colonPos = json.find(':', keyPos);
|
||||||
|
if (colonPos == std::string::npos) return;
|
||||||
|
size_t q1 = json.find('"', colonPos);
|
||||||
|
if (q1 == std::string::npos) return;
|
||||||
|
size_t q2 = json.find('"', q1 + 1);
|
||||||
|
if (q2 == std::string::npos) return;
|
||||||
|
|
||||||
|
std::string addr = json.substr(q1 + 1, q2 - q1 - 1);
|
||||||
|
if (addr.size() != 34 || addr[0] != 'T') {
|
||||||
|
printf("Bootstrap: snapshot-publisher.json contains invalid address '%s', ignoring\n",
|
||||||
|
addr.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_trustedPublisherMutex);
|
||||||
|
SetActiveTrustedSnapshotPublisherUnlocked(addr);
|
||||||
|
}
|
||||||
|
printf("Bootstrap: loaded trusted snapshot publisher override: %s\n", addr.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool PersistTrustedSnapshotPublisher(const std::string& addr)
|
||||||
|
{
|
||||||
|
fs::path filePath = GetDataDir(true) / SNAPSHOT_PUBLISHER_FILE;
|
||||||
|
std::ofstream f(filePath.string().c_str(), std::ios::trunc);
|
||||||
|
if (!f) return false;
|
||||||
|
f << "{\n"
|
||||||
|
<< " \"address\": \"" << addr << "\",\n"
|
||||||
|
<< " \"set_at\": " << GetTime() << ",\n"
|
||||||
|
<< " \"note\": \"Set via triangles-cli settrustedv2snapshotpublisher. "
|
||||||
|
<< "Replace atomically; previous publisher is dropped.\"\n"
|
||||||
|
<< "}\n";
|
||||||
|
return f.good();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SetTrustedSnapshotPublisher(const std::string& addr, std::string& strError)
|
||||||
|
{
|
||||||
|
if (addr.size() != 34 || addr[0] != 'T') {
|
||||||
|
strError = "settrustedv2snapshotpublisher: invalid address format (expected 34-char T-address)";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_trustedPublisherMutex);
|
||||||
|
SetActiveTrustedSnapshotPublisherUnlocked(addr);
|
||||||
|
}
|
||||||
|
if (!PersistTrustedSnapshotPublisher(addr)) {
|
||||||
|
strError = "settrustedv2snapshotpublisher: warning, could not persist to "
|
||||||
|
"snapshot-publisher.json (in-memory change is live for this session)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnsetTrustedSnapshotPublisher(std::string& strError)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_trustedPublisherMutex);
|
||||||
|
SetActiveTrustedSnapshotPublisherUnlocked(std::string());
|
||||||
|
}
|
||||||
|
fs::path filePath = GetDataDir(true) / SNAPSHOT_PUBLISHER_FILE;
|
||||||
|
fs::remove(filePath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool IsTrustedSnapshotSigner(const std::string& addr)
|
bool IsTrustedSnapshotSigner(const std::string& addr)
|
||||||
{
|
{
|
||||||
for (size_t i = 0; i < NUM_TRUSTED_SNAPSHOT_SIGNERS; ++i)
|
// 1. Runtime override (set via RPC).
|
||||||
if (addr == TRUSTED_SNAPSHOT_SIGNERS[i])
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_trustedPublisherMutex);
|
||||||
|
if (!g_activeTrustedSnapshotPublisher.empty() &&
|
||||||
|
addr == g_activeTrustedSnapshotPublisher)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 2. Built-in fallback (compiled in, read-only).
|
||||||
|
for (size_t i = 0; i < NUM_BUILTIN_TRUSTED_SNAPSHOT_SIGNERS; ++i)
|
||||||
|
if (addr == BUILTIN_TRUSTED_SNAPSHOT_SIGNERS[i])
|
||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,27 @@ namespace Bootstrap {
|
|||||||
ProgressCallback progressFn,
|
ProgressCallback progressFn,
|
||||||
std::string& strError);
|
std::string& strError);
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// Trusted snapshot publisher — RPC-driven single-slot rotation
|
||||||
|
// ===================================================================
|
||||||
|
// Returns the currently active trusted publisher, or empty string if
|
||||||
|
// only the built-in fallback is in effect.
|
||||||
|
std::string GetActiveTrustedSnapshotPublisher();
|
||||||
|
|
||||||
|
// Atomically replaces the active publisher. The previous one is dropped
|
||||||
|
// immediately (Design A: single-slot, no grace period). Persists to
|
||||||
|
// <datadir>/snapshot-publisher.json so the choice survives restarts.
|
||||||
|
bool SetTrustedSnapshotPublisher(const std::string& addr,
|
||||||
|
std::string& strError);
|
||||||
|
|
||||||
|
// Clears the runtime override and reverts to the built-in fallback
|
||||||
|
// list. Also removes snapshot-publisher.json from disk.
|
||||||
|
bool UnsetTrustedSnapshotPublisher(std::string& strError);
|
||||||
|
|
||||||
|
// Called once at daemon startup (from init.cpp) to load any persisted
|
||||||
|
// runtime override.
|
||||||
|
void LoadTrustedSnapshotPublisher();
|
||||||
|
|
||||||
} // namespace Bootstrap
|
} // namespace Bootstrap
|
||||||
|
|
||||||
#endif // TRIANGLES_BOOTSTRAP_H
|
#endif // TRIANGLES_BOOTSTRAP_H
|
||||||
|
|||||||
@@ -1191,6 +1191,9 @@ bool AppInit2()
|
|||||||
bool noBootstrap = GetBoolArg("-nobootstrap", false);
|
bool noBootstrap = GetBoolArg("-nobootstrap", false);
|
||||||
bool snapshotMode = GetBoolArg("-snapshot", true);
|
bool snapshotMode = GetBoolArg("-snapshot", true);
|
||||||
fs::path dataPath = GetDataDir();
|
fs::path dataPath = GetDataDir();
|
||||||
|
// Load any runtime trusted snapshot publisher override that was
|
||||||
|
// persisted by a previous settrustedv2snapshotpublisher call.
|
||||||
|
Bootstrap::LoadTrustedSnapshotPublisher();
|
||||||
bool needsBootstrap = Bootstrap::NeedsBootstrap(dataPath);
|
bool needsBootstrap = Bootstrap::NeedsBootstrap(dataPath);
|
||||||
|
|
||||||
if (needsBootstrap && !noBootstrap) {
|
if (needsBootstrap && !noBootstrap) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "utxosnapshot.h"
|
#include "utxosnapshot.h"
|
||||||
#include "checkpointpublisher.h"
|
#include "checkpointpublisher.h"
|
||||||
#include "wallet.h"
|
#include "wallet.h"
|
||||||
|
#include "bootstrap.h"
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|
||||||
@@ -1322,3 +1323,91 @@ Value dumputxoset(const Array& params, bool fHelp)
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Trusted snapshot publisher RPCs (Design A: single-slot rotation)
|
||||||
|
// ============================================================================
|
||||||
|
//
|
||||||
|
// settrustedv2snapshotpublisher <address>
|
||||||
|
// - Atomically replaces the active trusted snapshot publisher.
|
||||||
|
// - The previous publisher is dropped immediately (no grace period).
|
||||||
|
// - The new publisher is persisted to <datadir>/snapshot-publisher.json
|
||||||
|
// so the choice survives daemon restarts.
|
||||||
|
//
|
||||||
|
// gettrustedv2snapshotpublisher
|
||||||
|
// - Returns the currently active runtime override.
|
||||||
|
// - Empty string means no runtime override; built-in fallback list is
|
||||||
|
// the source of truth (which contains "TG8f76ykt...").
|
||||||
|
//
|
||||||
|
// unsettrustedv2snapshotpublisher
|
||||||
|
// - Clears the runtime override.
|
||||||
|
// - The built-in fallback list (read-only, compiled in) becomes the
|
||||||
|
// source of truth again.
|
||||||
|
// - Removes <datadir>/snapshot-publisher.json.
|
||||||
|
Value settrustedv2snapshotpublisher(const Array& params, bool fHelp)
|
||||||
|
{
|
||||||
|
if (fHelp || params.size() != 1)
|
||||||
|
throw runtime_error(
|
||||||
|
"settrustedv2snapshotpublisher <address>\n"
|
||||||
|
"Atomically replace the trusted snapshot publisher.\n"
|
||||||
|
"The previous publisher is dropped immediately (no grace period).\n"
|
||||||
|
"The new publisher is persisted to <datadir>/snapshot-publisher.json.\n"
|
||||||
|
"\nArguments:\n"
|
||||||
|
"1. address (string, required) Triangles T-address (34 chars, starts with 'T')\n"
|
||||||
|
"\nResult:\n"
|
||||||
|
"{ previous: 'T...', current: 'T...' } (previous is empty if first set)\n"
|
||||||
|
"\nExample:\n"
|
||||||
|
" triangles-cli settrustedv2snapshotpublisher TGotWuftzH7rD9tXC7whE8EXiyC3mr1CrH");
|
||||||
|
|
||||||
|
std::string addr = params[0].get_str();
|
||||||
|
std::string previous = Bootstrap::GetActiveTrustedSnapshotPublisher();
|
||||||
|
std::string err;
|
||||||
|
if (!Bootstrap::SetTrustedSnapshotPublisher(addr, err)) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, err);
|
||||||
|
}
|
||||||
|
Object result;
|
||||||
|
result.push_back(Pair("previous", previous));
|
||||||
|
result.push_back(Pair("current", addr));
|
||||||
|
if (!err.empty())
|
||||||
|
result.push_back(Pair("warning", err));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Value gettrustedv2snapshotpublisher(const Array& params, bool fHelp)
|
||||||
|
{
|
||||||
|
if (fHelp || params.size() != 0)
|
||||||
|
throw runtime_error(
|
||||||
|
"gettrustedv2snapshotpublisher\n"
|
||||||
|
"Returns the currently active trusted snapshot publisher.\n"
|
||||||
|
"Empty string means no runtime override is set; the built-in\n"
|
||||||
|
"fallback list (compiled in) is the source of truth.\n"
|
||||||
|
"\nResult:\n"
|
||||||
|
"{ active: 'T...', has_runtime_override: true|false }");
|
||||||
|
|
||||||
|
std::string active = Bootstrap::GetActiveTrustedSnapshotPublisher();
|
||||||
|
Object result;
|
||||||
|
result.push_back(Pair("active", active));
|
||||||
|
result.push_back(Pair("has_runtime_override", !active.empty()));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Value unsettrustedv2snapshotpublisher(const Array& params, bool fHelp)
|
||||||
|
{
|
||||||
|
if (fHelp || params.size() != 0)
|
||||||
|
throw runtime_error(
|
||||||
|
"unsettrustedv2snapshotpublisher\n"
|
||||||
|
"Clear the runtime trusted snapshot publisher override.\n"
|
||||||
|
"The built-in fallback list (compiled in) becomes the source of truth again.\n"
|
||||||
|
"Removes <datadir>/snapshot-publisher.json.\n"
|
||||||
|
"\nResult:\n"
|
||||||
|
"{ unset: true, fallback_in_effect: true }");
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
if (!Bootstrap::UnsetTrustedSnapshotPublisher(err)) {
|
||||||
|
throw JSONRPCError(RPC_INTERNAL_ERROR, err);
|
||||||
|
}
|
||||||
|
Object result;
|
||||||
|
result.push_back(Pair("unset", true));
|
||||||
|
result.push_back(Pair("fallback_in_effect", true));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
@@ -317,7 +317,10 @@ static const CRPCCommand vRPCCommands[] =
|
|||||||
{ "getcheckpoint", &getcheckpoint, true, false },
|
{ "getcheckpoint", &getcheckpoint, true, false },
|
||||||
{ "gencheckpoints", &gencheckpoints, true, false },
|
{ "gencheckpoints", &gencheckpoints, true, false },
|
||||||
{ "publishcheckpoint", &publishcheckpoint, true, false },
|
{ "publishcheckpoint", &publishcheckpoint, true, false },
|
||||||
{ "getchaintips", &getchaintips, true, false },
|
{ "settrustedv2snapshotpublisher", &settrustedv2snapshotpublisher, false, false },
|
||||||
|
{ "gettrustedv2snapshotpublisher", &gettrustedv2snapshotpublisher, false, false },
|
||||||
|
{ "unsettrustedv2snapshotpublisher", &unsettrustedv2snapshotpublisher, false, false },
|
||||||
|
{ "getchaintips", &getchaintips, true, false },
|
||||||
{ "invalidateblock", &invalidateblock, false, false },
|
{ "invalidateblock", &invalidateblock, false, false },
|
||||||
{ "reconsiderblock", &reconsiderblock, false, false },
|
{ "reconsiderblock", &reconsiderblock, false, false },
|
||||||
{ "recalculatesupply", &recalculatesupply, false, false },
|
{ "recalculatesupply", &recalculatesupply, false, false },
|
||||||
|
|||||||
@@ -228,6 +228,9 @@ extern json_spirit::Value getblockbynumber(const json_spirit::Array& params, boo
|
|||||||
extern json_spirit::Value getcheckpoint(const json_spirit::Array& params, bool fHelp);
|
extern json_spirit::Value getcheckpoint(const json_spirit::Array& params, bool fHelp);
|
||||||
extern json_spirit::Value gencheckpoints(const json_spirit::Array& params, bool fHelp);
|
extern json_spirit::Value gencheckpoints(const json_spirit::Array& params, bool fHelp);
|
||||||
extern json_spirit::Value publishcheckpoint(const json_spirit::Array& params, bool fHelp);
|
extern json_spirit::Value publishcheckpoint(const json_spirit::Array& params, bool fHelp);
|
||||||
|
extern json_spirit::Value settrustedv2snapshotpublisher(const json_spirit::Array& params, bool fHelp);
|
||||||
|
extern json_spirit::Value gettrustedv2snapshotpublisher(const json_spirit::Array& params, bool fHelp);
|
||||||
|
extern json_spirit::Value unsettrustedv2snapshotpublisher(const json_spirit::Array& params, bool fHelp);
|
||||||
extern json_spirit::Value getchaintips(const json_spirit::Array& params, bool fHelp);
|
extern json_spirit::Value getchaintips(const json_spirit::Array& params, bool fHelp);
|
||||||
extern json_spirit::Value invalidateblock(const json_spirit::Array& params, bool fHelp);
|
extern json_spirit::Value invalidateblock(const json_spirit::Array& params, bool fHelp);
|
||||||
extern json_spirit::Value reconsiderblock(const json_spirit::Array& params, bool fHelp);
|
extern json_spirit::Value reconsiderblock(const json_spirit::Array& params, bool fHelp);
|
||||||
|
|||||||
Reference in New Issue
Block a user