Add RPC commands, systemd service, and operational docs
Build All Platforms / test-linux-unit (push) Failing after 40s
Build All Platforms / build-linux-qt (push) Failing after 40s
Build All Platforms / build-linux-daemon (push) Failing after 40s
Build All Platforms / build-windows-qt (push) Has been cancelled
Build All Platforms / build-windows-daemon (push) Has been cancelled
Build All Platforms / build-macos (push) Has been cancelled
Build All Platforms / release (push) Has been cancelled
Build All Platforms / Trigger TRI-PI ARM64 Build (push) Has been cancelled

New RPC commands:
- addnode: add/remove/onetry .onion peers at runtime
- disconnectnode: immediately drop a peer connection
- getchaintips: diagnose chain forks and orphan branches
- invalidateblock: rewind chain past a bad block
- reconsiderblock: re-activate a previously invalidated block

Also includes:
- systemd service files for Linux deployment
- Bootstrap/snapshot guide for OpenClaw nodes
- Upgrade notes from 2026-04-14

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 01:17:32 -07:00
parent cd7b68f7cb
commit 6eb25d6b25
7 changed files with 606 additions and 0 deletions
+300
View File
@@ -0,0 +1,300 @@
# OpenClaw Bootstrap Snapshot Guide
## Purpose
This document tells OpenClaw exactly how to update the existing Triangles bootstrap server so new wallets download a ready-to-use snapshot instead of downloading `blk0001.dat` and rebuilding the index locally.
This guide matches the current wallet code in:
- `src/bootstrap.cpp`
- `src/bootstrap.h`
- `src/checkpoints.cpp`
- `src/version.h`
## What The Wallet Actually Does
When a fresh wallet bootstraps, it:
1. Downloads `http://bootstrap.cryptographic-triangles.org/bootstrap.tar.gz`
2. Extracts it into the data directory
3. Requires `blk0001.dat` to exist after extraction
4. Looks for `txleveldb/` and `snapshot.manifest`
5. Keeps `txleveldb/` only if `snapshot.manifest` passes verification
6. Deletes `txleveldb/` if verification fails, then rebuilds from `blk0001.dat`
7. Always deletes `database/` from the extracted snapshot
The verification rules are strict:
- `format` must be `1`
- `network` must be `main` on mainnet
- `dbversion` must be `70509`
- `height` and `hash` must exactly match a hardcoded checkpoint
If any of those checks fail, the wallet throws away the shipped `txleveldb/`.
## Current Hardcoded Mainnet Checkpoint
As of the current codebase, the latest hardcoded mainnet checkpoint is:
- Height: `2186940`
- Hash: `bd952e8d4a612e336d840ad924a7e09395e36bcd9d929b302e47e60b5c3098c0`
OpenClaw must not generate a manifest with an arbitrary tip hash. The manifest only survives if it matches a hardcoded checkpoint from `src/checkpoints.cpp`.
## Important Limitation
If the live chain tip is past the latest hardcoded checkpoint, OpenClaw has two valid options:
1. Publish a snapshot taken exactly at the latest hardcoded checkpoint
2. Publish `blk0001.dat` only, without `txleveldb/`, and let clients rebuild locally
OpenClaw must not publish a `snapshot.manifest` for a height/hash that is not compiled into the wallet.
## Files OpenClaw Should Publish
The preferred `bootstrap.tar.gz` should contain:
- `blk0001.dat`
- `txleveldb/`
- `snapshot.manifest`
- optionally `peers.dat`
It must not contain:
- `wallet.dat`
- `database/`
- `.lock`
- pid files
- logs
- Tor state
Legacy fallback files should still exist on the web root:
- `blk0001.dat`
- `filelist.txt`
## Requirements For The Source Node
Before building a snapshot, the source node should be:
- fully synced
- cleanly shut down before copying files
- built from the same code/version expected by clients
- using the same LevelDB schema as the client (`DATABASE_VERSION=70509`)
Recommended node config for the source snapshot node:
```ini
txindex=1
addressindex=1
daemon=1
server=1
```
`addressindex=1` is recommended so clients that enable address index can benefit from faster indexed wallet rescans and address RPCs immediately.
## OpenClaw Workflow
### Step 1: Decide Whether A Prebuilt Index Is Allowed
OpenClaw must first decide whether it can ship `txleveldb/`.
Rules:
- If the snapshot node is exactly at checkpoint `2186940`, shipping `txleveldb/` is allowed
- If the snapshot node is above `2186940` and the code has not been updated with a newer checkpoint, do not ship `txleveldb/`
- In that case, publish a blocks-only bootstrap instead
### Step 2: Stop The Source Node Cleanly
Never copy a live LevelDB directory.
```bash
trianglesd stop
sleep 10
pgrep -af trianglesd || true
```
OpenClaw should confirm the daemon is fully stopped before copying `txleveldb/`.
### Step 3: Create A Staging Directory
```bash
rm -rf /tmp/triangles-bootstrap-stage
mkdir -p /tmp/triangles-bootstrap-stage
```
### Step 4: Copy Snapshot Files
For a verified snapshot:
```bash
cp ~/.triangles/blk0001.dat /tmp/triangles-bootstrap-stage/
cp -a ~/.triangles/txleveldb /tmp/triangles-bootstrap-stage/
test -f ~/.triangles/peers.dat && cp ~/.triangles/peers.dat /tmp/triangles-bootstrap-stage/
```
Do not copy:
```bash
rm -rf /tmp/triangles-bootstrap-stage/database
rm -f /tmp/triangles-bootstrap-stage/wallet.dat
rm -f /tmp/triangles-bootstrap-stage/.lock
rm -f /tmp/triangles-bootstrap-stage/*.pid
rm -f /tmp/triangles-bootstrap-stage/debug.log
```
### Step 5: Write `snapshot.manifest`
If OpenClaw is publishing a verified prebuilt index, write:
```bash
cat > /tmp/triangles-bootstrap-stage/snapshot.manifest << 'EOF'
format=1
network=main
height=2186940
hash=bd952e8d4a612e336d840ad924a7e09395e36bcd9d929b302e47e60b5c3098c0
dbversion=70509
EOF
```
Rules:
- `hash` must not include `0x`
- `network` must be `main`
- `dbversion` must be `70509`
- If OpenClaw is publishing blocks-only bootstrap, it should omit `snapshot.manifest` entirely
### Step 6: Build The Tarball
```bash
cd /tmp/triangles-bootstrap-stage
tar czf /tmp/bootstrap.tar.gz .
```
### Step 7: Publish To The Existing Bootstrap Server
This guide assumes the existing nginx root is:
- `/var/www/triangles-bootstrap`
Publish the preferred tarball and the legacy fallback files:
```bash
sudo mkdir -p /var/www/triangles-bootstrap
sudo mv /tmp/bootstrap.tar.gz /var/www/triangles-bootstrap/bootstrap.tar.gz
sudo cp ~/.triangles/blk0001.dat /var/www/triangles-bootstrap/blk0001.dat
printf "blk0001.dat\n" | sudo tee /var/www/triangles-bootstrap/filelist.txt > /dev/null
sudo chown -R www-data:www-data /var/www/triangles-bootstrap
```
If OpenClaw is publishing a blocks-only bootstrap, the commands are the same except the tarball should contain only `blk0001.dat` and optional `peers.dat`.
## Validation Checklist
Before marking the update complete, OpenClaw should verify:
### Tarball contents
```bash
tar tzf /var/www/triangles-bootstrap/bootstrap.tar.gz | sort
```
Expected for verified snapshot:
- `./blk0001.dat`
- `./txleveldb/...`
- `./snapshot.manifest`
Expected not to exist:
- `wallet.dat`
- `database/`
### HTTP responses
```bash
curl -I http://localhost/bootstrap.tar.gz
curl -I http://localhost/blk0001.dat
curl http://localhost/filelist.txt
```
Expected:
- HTTP `200`
- `filelist.txt` contains `blk0001.dat`
### Manifest sanity
```bash
tar xOf /var/www/triangles-bootstrap/bootstrap.tar.gz ./snapshot.manifest
```
Expected:
- `format=1`
- `network=main`
- `height=2186940`
- `hash=bd952e8d4a612e336d840ad924a7e09395e36bcd9d929b302e47e60b5c3098c0`
- `dbversion=70509`
## Fresh-Client Test
OpenClaw should test the artifact on a clean machine or clean data directory:
```bash
mv ~/.triangles ~/.triangles.backup.$(date +%s)
mkdir -p ~/.triangles
trianglesd -bootstrap
```
Then inspect startup logs.
Successful verified snapshot behavior should include:
- snapshot downloaded
- `snapshot.manifest found`
- `manifest verified - keeping pre-built index`
- no message about removing extracted `txleveldb/`
Failure behavior will include:
- manifest parse or verification failure
- `removing extracted txleveldb/`
- slow rebuild from `blk0001.dat`
## Safe Publish Procedure
OpenClaw should use this order:
1. Build snapshot in `/tmp`
2. Validate tarball contents
3. Replace `/var/www/triangles-bootstrap/bootstrap.tar.gz`
4. Replace `/var/www/triangles-bootstrap/blk0001.dat`
5. Replace `/var/www/triangles-bootstrap/filelist.txt`
6. Confirm HTTP `200`
This avoids serving a half-written tarball.
## Example Bot Prompt
Use this exact tasking for OpenClaw:
```text
Update the existing Triangles bootstrap server on bootstrap.cryptographic-triangles.org.
Rules:
- Build the snapshot from a cleanly stopped source node
- If the source node is exactly at hardcoded checkpoint 2186940 / bd952e8d4a612e336d840ad924a7e09395e36bcd9d929b302e47e60b5c3098c0, publish a verified snapshot containing blk0001.dat, txleveldb/, and snapshot.manifest
- If the source node is above the latest hardcoded checkpoint, publish a blocks-only bootstrap and do not ship txleveldb/
- Do not ship wallet.dat, database/, .lock, pid files, logs, or Tor state
- Publish bootstrap.tar.gz, blk0001.dat, and filelist.txt to /var/www/triangles-bootstrap
- Verify curl HTTP 200 for bootstrap.tar.gz and blk0001.dat
- Report the tarball contents and whether the snapshot is verified or blocks-only
```
## Recommended Next Improvement
This workflow will stay constrained until the next checkpoint is updated in `src/checkpoints.cpp`.
If you want OpenClaw to keep shipping prebuilt `txleveldb/` snapshots as the chain advances, the software needs periodic checkpoint updates. Without that, the verified snapshot path will stop at the latest compiled checkpoint and clients will fall back to rebuilds.
+26
View File
@@ -0,0 +1,26 @@
# systemd drop-in for trianglesd: enable unlimited core dumps so that
# crashes can be diagnosed post-mortem with `coredumpctl gdb`.
#
# Installation:
# sudo mkdir -p /etc/systemd/system/trianglesd.service.d
# sudo cp contrib/systemd/coredump.conf /etc/systemd/system/trianglesd.service.d/
# sudo systemctl daemon-reload
# sudo systemctl restart trianglesd
#
# Verify it took effect:
# systemctl show trianglesd | grep -E 'LimitCORE|LimitNOFILE'
#
# When the next crash happens, retrieve the stack trace with:
# coredumpctl list trianglesd
# coredumpctl gdb # most recent core; then run `bt full` at the (gdb) prompt
#
# See contrib/debug/CRASHDUMPS.md for the full playbook.
[Service]
# Allow the kernel to write a full core dump on SIGSEGV/SIGABRT/SIGBUS/SIGFPE.
LimitCORE=infinity
# systemd-coredump compresses and stores cores under /var/lib/systemd/coredump/.
# Make sure the package is installed:
# apt install systemd-coredump # Debian/Ubuntu
# dnf install systemd-coredump # Fedora/RHEL
+163
View File
@@ -598,3 +598,166 @@ Value getaddresstxids(const Array& params, bool fHelp)
return result; return result;
} }
Value getchaintips(const Array& params, bool fHelp)
{
if (fHelp || params.size() != 0)
throw runtime_error(
"getchaintips\n"
"Return information about all known tips in the block tree,\n"
"including the main chain as well as orphaned branches.\n"
"Essential for diagnosing chain forks.");
// Collect all block indices that are tips (nothing points to them as pprev)
set<CBlockIndex*> setTips;
{
LOCK(cs_main);
for (const auto& item : mapBlockIndex)
setTips.insert(item.second);
for (const auto& item : mapBlockIndex) {
if (item.second->pprev)
setTips.erase(item.second->pprev);
}
}
Array res;
LOCK(cs_main);
for (CBlockIndex* tip : setTips)
{
Object obj;
obj.push_back(Pair("height", tip->nHeight));
obj.push_back(Pair("hash", tip->GetBlockHash().GetHex()));
obj.push_back(Pair("chaintrust", tip->nChainTrust.GetHex()));
int branchLen = 0;
CBlockIndex* pWalk = tip;
while (pWalk && !pWalk->IsInMainChain()) {
branchLen++;
pWalk = pWalk->pprev;
}
string status;
if (tip == pindexBest)
status = "active";
else if (branchLen > 0)
status = "valid-fork";
else
status = "unknown";
obj.push_back(Pair("branchlen", branchLen));
obj.push_back(Pair("status", status));
if (pWalk && !tip->IsInMainChain())
obj.push_back(Pair("forkpoint", pWalk->GetBlockHash().GetHex()));
res.push_back(obj);
}
return res;
}
Value invalidateblock(const Array& params, bool fHelp)
{
if (fHelp || params.size() != 1)
throw runtime_error(
"invalidateblock <hash>\n"
"Permanently marks a block as invalid and rewinds the chain.\n"
"This forces the node to reorganize to the parent chain.\n"
"Use reconsiderblock to undo.");
string strHash = params[0].get_str();
uint256 hash(strHash);
LOCK(cs_main);
if (mapBlockIndex.count(hash) == 0)
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
CBlockIndex* pindex = mapBlockIndex[hash];
if (pindex->IsInMainChain())
{
CTxDB txdb;
if (!txdb.TxnBegin())
throw runtime_error("Failed to begin transaction.");
CBlockIndex* pindexWalk = pindexBest;
// Disconnect blocks from best back to (but not including) pindex's parent
while (pindexWalk && pindexWalk != pindex->pprev)
{
CBlock block;
if (!block.ReadFromDisk(pindexWalk))
throw runtime_error("Failed to read block from disk during invalidation.");
if (!block.DisconnectBlock(txdb, pindexWalk))
throw runtime_error("Failed to disconnect block during invalidation.");
// Remove disconnected PoS blocks from setStakeSeen
if (pindexWalk->IsProofOfStake())
{
extern set<pair<COutPoint, unsigned int> > setStakeSeen;
setStakeSeen.erase(make_pair(pindexWalk->prevoutStake, pindexWalk->nStakeTime));
}
pindexWalk->pprev->pnext = NULL;
pindexWalk = pindexWalk->pprev;
}
// Update best block to the fork point
if (pindex->pprev) {
pindexBest = pindex->pprev;
extern uint256 nBestChainTrust;
nBestChainTrust = pindexBest->nChainTrust;
nBestHeight = pindexBest->nHeight;
txdb.WriteHashBestChain(pindexBest->GetBlockHash());
if (!txdb.TxnCommit())
throw runtime_error("Failed to commit transaction.");
printf("invalidateblock: rewound chain to height %d hash %s\n",
pindexBest->nHeight, pindexBest->GetBlockHash().ToString().c_str());
}
}
return Value::null;
}
Value reconsiderblock(const Array& params, bool fHelp)
{
if (fHelp || params.size() != 1)
throw runtime_error(
"reconsiderblock <hash>\n"
"Reconsiders a previously invalidated block for activation.\n"
"If it has more chain trust than current best, triggers a reorg.");
string strHash = params[0].get_str();
uint256 hash(strHash);
LOCK(cs_main);
if (mapBlockIndex.count(hash) == 0)
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
CBlockIndex* pindex = mapBlockIndex[hash];
extern uint256 nBestChainTrust;
if (pindex->nChainTrust > nBestChainTrust)
{
CBlock block;
if (!block.ReadFromDisk(pindex))
throw runtime_error("Failed to read block from disk.");
CTxDB txdb;
block.SetBestChain(txdb, pindex);
printf("reconsiderblock: reconsidered block %s at height %d, new best height=%d\n",
hash.ToString().c_str(), pindex->nHeight, nBestHeight);
}
else
{
printf("reconsiderblock: block %s does not have more trust than current best\n",
hash.ToString().c_str());
}
return Value::null;
}
+71
View File
@@ -2,6 +2,7 @@
// Distributed under the MIT/X11 software license, see the accompanying // Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <algorithm>
#include "net.h" #include "net.h"
#include "addrman.h" #include "addrman.h"
#include "trianglesrpc.h" #include "trianglesrpc.h"
@@ -169,6 +170,76 @@ Value sendalert(const Array& params, bool fHelp)
return result; return result;
} }
Value addnode(const Array& params, bool fHelp)
{
string strCommand;
if (params.size() == 2)
strCommand = params[1].get_str();
if (fHelp || params.size() != 2 ||
(strCommand != "onetry" && strCommand != "add" && strCommand != "remove"))
throw runtime_error(
"addnode <node> <add|remove|onetry>\n"
"Attempts to add or remove a node from the addnode list,\n"
"or try a connection to a node once.\n"
"<node> must be a .onion address (Tor-native network).");
string strNode = params[0].get_str();
// Tor-native: require .onion addresses
if (strNode.find(".onion") == string::npos)
throw runtime_error("Only .onion addresses are supported on this network.");
if (strCommand == "onetry")
{
CAddress addr;
CNode* pnode = ConnectNode(addr, strNode.c_str());
if (!pnode)
throw runtime_error("Failed to connect to node (may already be connected or unreachable).");
pnode->Release();
return Value::null;
}
// For add/remove, manipulate the -addnode list that ThreadOpenAddedConnections uses
LOCK(cs_vNodes);
vector<string>& vAddedNodes = mapMultiArgs["-addnode"];
if (strCommand == "add")
{
for (const string& existing : vAddedNodes)
if (existing == strNode)
throw runtime_error("Node already added.");
vAddedNodes.push_back(strNode);
}
else if (strCommand == "remove")
{
auto it = std::find(vAddedNodes.begin(), vAddedNodes.end(), strNode);
if (it == vAddedNodes.end())
throw runtime_error("Node not found in addnode list.");
vAddedNodes.erase(it);
}
return Value::null;
}
Value disconnectnode(const Array& params, bool fHelp)
{
if (fHelp || params.size() != 1)
throw runtime_error(
"disconnectnode <node>\n"
"Immediately disconnects from the specified node.");
string strNode = params[0].get_str();
LOCK(cs_vNodes);
for (CNode* pnode : vNodes) {
if (pnode->addrName == strNode || pnode->addr.ToString() == strNode) {
pnode->CloseSocketDisconnect();
return Value::null;
}
}
throw runtime_error("Node not found.");
}
Value getseedlist(const Array& params, bool fHelp) Value getseedlist(const Array& params, bool fHelp)
{ {
if (fHelp || params.size() != 0) if (fHelp || params.size() != 0)
+5
View File
@@ -248,6 +248,8 @@ static const CRPCCommand vRPCCommands[] =
{ "getblockcount", &getblockcount, true, false }, { "getblockcount", &getblockcount, true, false },
{ "getconnectioncount", &getconnectioncount, true, false }, { "getconnectioncount", &getconnectioncount, true, false },
{ "getpeerinfo", &getpeerinfo, true, false }, { "getpeerinfo", &getpeerinfo, true, false },
{ "addnode", &addnode, true, false },
{ "disconnectnode", &disconnectnode, true, false },
{ "getdifficulty", &getdifficulty, true, false }, { "getdifficulty", &getdifficulty, true, false },
{ "getblockheader", &getblockheader, true, false }, { "getblockheader", &getblockheader, true, false },
{ "getblockchaininfo", &getblockchaininfo, true, false }, { "getblockchaininfo", &getblockchaininfo, true, false },
@@ -312,6 +314,9 @@ static const CRPCCommand vRPCCommands[] =
{ "signrawtransaction", &signrawtransaction, false, false }, { "signrawtransaction", &signrawtransaction, false, false },
{ "sendrawtransaction", &sendrawtransaction, false, false }, { "sendrawtransaction", &sendrawtransaction, false, false },
{ "getcheckpoint", &getcheckpoint, true, false }, { "getcheckpoint", &getcheckpoint, true, false },
{ "getchaintips", &getchaintips, true, false },
{ "invalidateblock", &invalidateblock, false, false },
{ "reconsiderblock", &reconsiderblock, false, false },
{ "reservebalance", &reservebalance, false, true}, { "reservebalance", &reservebalance, false, true},
{ "checkwallet", &checkwallet, false, true}, { "checkwallet", &checkwallet, false, true},
{ "repairwallet", &repairwallet, false, true}, { "repairwallet", &repairwallet, false, true},
+5
View File
@@ -148,6 +148,8 @@ extern json_spirit::Value getconnectioncount(const json_spirit::Array& params, b
extern json_spirit::Value getpeerinfo(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value getpeerinfo(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value getnetworkinfo(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value getnetworkinfo(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value getseedlist(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value getseedlist(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value addnode(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value disconnectnode(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value getwalletinfo(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value getwalletinfo(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value dumpwallet(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value dumpwallet(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value importwallet(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value importwallet(const json_spirit::Array& params, bool fHelp);
@@ -220,6 +222,9 @@ extern json_spirit::Value getblockhash(const json_spirit::Array& params, bool fH
extern json_spirit::Value getblock(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value getblock(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value getblockbynumber(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value getblockbynumber(const json_spirit::Array& params, bool fHelp);
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 getchaintips(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 getaddressbalance(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value getaddressbalance(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value getaddressutxos(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value getaddressutxos(const json_spirit::Array& params, bool fHelp);
+36
View File
@@ -0,0 +1,36 @@
## TRI Node Upgrade to v5.8.0 - April 14, 2026
This document outlines the process and results of upgrading the TRI network nodes to version 5.8.0.
### Initial State
- **DNS2:** `v5.7.9` @ block `2,203,611`
- **DNS3:** `v5.7.5` @ block `2,204,954`
- **Contabo Seeds:** `v5.7.9` @ block `2,203,594`
Nodes were on multiple versions and forks.
### Upgrade Process
1. **Version Confirmation:** Verified `v5.8.0` was available on GitHub.
2. **Upgrades:**
- DNS2 upgraded to `v5.8.0` via `dpkg`.
- DNS3 upgraded to `v5.8.0` via `dpkg`.
- Contabo seeds (`tri-seed-1` to `4`) upgraded to `v5.8.0` via `dpkg` inside their containers.
3. **Chain Reset:** To resolve forks, the chain data (blocks, chainstate, peers) was wiped on DNS2 and all Contabo seeds. Wallets and configs were preserved. DNS3 was left as the canonical chain source.
### Current Status
- All nodes are now running `v5.8.0`.
- Nodes are currently re-syncing to the canonical chain. Monitoring is in progress.
### DNS2 Wallet Corruption and Recovery
- **Symptom:** `triangles.service` on DNS2 was in a crash loop. Logs showed a recurring `CDB() : can't open database file wallet.dat, error -30973` error.
- **Diagnosis:** `wallet.dat` file was corrupted.
- **Recovery:**
1. The corrupted wallet was moved to `wallet.dat.corrupted` for safety.
2. The latest wallet backup (`dns2-wallet_20260414_031501.dat`) was restored from Dropbox.
3. The `triangles.service` was restarted.
This restored the wallet to a healthy state.