Skip to content
Closed
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
76 changes: 76 additions & 0 deletions .qoder/docs/witness-guard-spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "witness-guard-spec.json",
"title": "Witness Guard Plugin Specification",
"description": "Technical specification for the automated witness key restoration plugin",
"version": "1.0.0",

"definitions": {
"witness_entry": {
"type": "array",
"description": "A JSON array representing a witness configuration triplet",
"items": [
{ "name": "account_name", "type": "string", "description": "The name of the witness account to monitor" },
{ "name": "signing_wif", "type": "string", "description": "The WIF private key to be restored as the signing key" },
{ "name": "active_wif", "type": "string", "description": "The WIF private key with active authority to sign the update transaction" }
],
"example": "[\"mywitness\", \"5Ksigning...\", \"5Kactive...\"]"
}
},

"configuration": {
"options": [
{
"name": "witness-guard-enabled",
"type": "boolean",
"default": true,
"description": "Global toggle for the plugin logic"
},
{
"name": "witness-guard-witness",
"type": "vector<string>",
"description": "List of witness triplets in JSON format. Can be specified multiple times.",
"ref": "#/definitions/witness_entry"
},
{
"name": "witness-guard-interval",
"type": "uint32",
"default": 20,
"description": "Frequency of checks measured in blocks (20 blocks is approx. 60 seconds)"
}
]
},

"algorithms": {
"check_and_restore": {
"description": "Iterative process to detect and fix null signing keys",
"steps": [
{ "step": 1, "action": "Verify node sync: head_block_time must be within 2 * CHAIN_BLOCK_INTERVAL" },
{ "step": 2, "action": "Fetch witness index from database" },
{ "step": 3, "action": "For each configured witness: find record by name" },
{ "step": 4, "condition": "signing_key is NOT null", "action": "Remove witness from 'restore_sent' cache to allow future monitoring" },
{ "step": 5, "condition": "signing_key IS null AND NOT in 'restore_sent'", "action": "Invoke send_witness_update" }
]
},

"send_witness_update": {
"description": "Constructs and broadcasts a witness_update_operation",
"steps": [
{ "step": 1, "action": "Extract current URL from the chain object (preserves metadata)" },
{ "step": 2, "action": "Create witness_update_operation with target signing_pub_key" },
{ "step": 3, "action": "Wrap operation in a signed_transaction" },
{ "step": 4, "action": "Set expiration (30s) and reference block (head)" },
{ "step": 5, "action": "Sign with the configured active_private_key" },
{ "step": 6, "action": "Broadcast via P2P plugin" },
{ "step": 7, "action": "Add witness name to 'restore_sent' cache to prevent duplicate broadcasts" }
]
}
},

"internal_state": {
"restore_sent": {
"type": "std::set<string>",
"description": "In-memory cache of witnesses for which a restoration transaction has been broadcast in the current block cycle or since the last key reset"
}
}
}
87 changes: 87 additions & 0 deletions .qoder/docs/witness-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Witness Guard Plugin

The `witness_guard` plugin is an automated maintenance tool designed for VIZ witness node operators. Its primary purpose is to monitor configured witness accounts and automatically restore their signing keys if they are reset to a null state (effectively disabling the witness).

## Purpose

In Graphene-based networks like VIZ, a witness might be disabled (signing key set to null) due to manual intervention, security protocols, or certain network conditions. If an operator wants to ensure their witness stays active without manual monitoring, this plugin automates the "restore" process.

When the plugin detects a null signing key on-chain for a monitored account, it constructs, signs, and broadcasts a `witness_update_operation` to re-enable the witness using the provided private keys.

## Configuration

The plugin can be configured via the `config.ini` file or command-line arguments.

### Options

| Option | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `witness-guard-enabled` | boolean | `true` | Enables or disables the plugin logic globally. |
| `witness-guard-interval` | uint32 | `20` | Frequency of checks measured in blocks (default 20 blocks $\approx$ 60 seconds). |
| `witness-guard-witness` | vector\<string\> | N/A | A JSON triplet containing the witness name, the signing WIF, and the active WIF. |

### Enabling the Plugin

To use the plugin, add it to the list of active plugins in your `config.ini`:

```ini
plugin = witness_guard
```

## Usage Example

To monitor one or more witnesses, add the following lines to your `config.ini`. Note that the witness configuration must be a valid JSON array string.

```ini
# Automatically restore winet1
witness-guard-witness = ["winet1", "5K_SIGNING_PRIVATE_WIF", "5K_ACTIVE_PRIVATE_WIF"]

# You can monitor multiple witnesses by repeating the option
witness-guard-witness = ["winet2", "5J_SIGNING_PRIVATE_WIF", "5J_ACTIVE_PRIVATE_WIF"]

# Check every 10 blocks instead of 20
witness-guard-interval = 10
```

## Internal Logic (How it works)

1. **Sync Check**: The plugin only executes if the node's head block time is within a reasonable range (2 * `CHAIN_BLOCK_INTERVAL`), ensuring it doesn't attempt restores while the node is still catching up to the network.
2. **On-Chain Verification**: Every `X` blocks (configured by interval), the plugin looks up the witness object for the configured account names.
3. **Null Key Detection**: If the `block_signing_key` found on the blockchain matches the `null_key` (all zeros):
* The plugin prepares a `witness_update_operation`.
* It preserves the existing `url` from the on-chain object.
* It sets the `block_signing_key` to the public key derived from the provided `signing_wif`.
4. **Signing and Broadcast**: The transaction is signed using the `active_wif`. This is necessary because updating a witness object requires active authority.
5. **Anti-Spam Protection**: Once a restore transaction is sent, the plugin will not attempt to send another one for that specific witness until the node is restarted or the key is successfully reset on-chain (preventing a loop of transactions if the first one is pending).

## Security Considerations

> [!CAUTION]
> **Private Key Exposure**
> This plugin requires the **Active Private Key** to be stored in plain text within your `config.ini`. Because the active key has significant control over your account (including the ability to transfer funds or change permissions), ensure that your `config.ini` file has strictly restricted file system permissions (e.g., `chmod 600 config.ini` on Linux).

## Logs

The plugin provides clear feedback in the node logs:

* **Initialization**:
`witness_guard: monitoring witness 'winet1' (signing key: VIZ...)`
* **Restore Triggered**:
`witness_guard: 'winet1' has null signing key on-chain — initiating restore`
* **Success**:
`witness_guard: witness_update for 'winet1' sent successfully`
* **Failure**:
`witness_guard: witness_update FAILED for 'winet1': [error details]`

## Troubleshooting

**Erorr: witness-guard-witness must be triplets**
Ensure each entry is a valid JSON array with exactly 3 strings: `["name", "signing_wif", "active_wif"]`. Ensure you are using double quotes for strings inside the brackets.

**Restore not triggering**
1. Check if `witness-guard-enabled` is set to `true`.
2. Ensure the node is fully synchronized with the network.
3. Verify that the account name provided is an actual registered witness on the network.

**Transaction failed**
Check that the `active_wif` provided actually belongs to the witness account. If the account's active authority has been changed, the plugin will be unable to sign the update.
1 change: 1 addition & 0 deletions build-linux.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env bash

# ============================================================================
# VIZ Linux Build Script
#
Expand Down
16 changes: 0 additions & 16 deletions libraries/chain/database.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1456,22 +1456,6 @@ namespace graphene { namespace chain {
throw;
}

// Prune stale competing blocks from dead forks at this height.
// A competing block is only safe to remove if its parent is no longer
// in the fork_db — without a parent the fork cannot be extended, so
// the block is truly dead. Removing blocks whose parent is still known
// would break legitimate fork switches when later blocks arrive.
{
auto competing = _fork_db.fetch_block_by_number(new_block.block_num());
for (const auto& cb : competing) {
if (cb->id != new_block.id() && !_fork_db.is_known_block(cb->data.previous)) {
wlog("Pruning stale competing block ${id} at height ${n} from fork_db (dead fork)",
("id", cb->id)("n", new_block.block_num()));
_fork_db.remove(cb->id);
}
}
}

return false;
} FC_CAPTURE_AND_RETHROW()
}
Expand Down
44 changes: 44 additions & 0 deletions plugins/witness_guard/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
set(CURRENT_TARGET witness_guard)

list(APPEND CURRENT_TARGET_HEADERS
include/graphene/plugins/witness_guard/witness_guard.hpp
)

list(APPEND CURRENT_TARGET_SOURCES
witness_guard.cpp
)

if(BUILD_SHARED_LIBRARIES)
add_library(graphene_${CURRENT_TARGET} SHARED
${CURRENT_TARGET_HEADERS}
${CURRENT_TARGET_SOURCES}
)
else()
add_library(graphene_${CURRENT_TARGET} STATIC
${CURRENT_TARGET_HEADERS}
${CURRENT_TARGET_SOURCES}
)
endif()

add_library(graphene::${CURRENT_TARGET} ALIAS graphene_${CURRENT_TARGET})
set_property(TARGET graphene_${CURRENT_TARGET} PROPERTY EXPORT_NAME ${CURRENT_TARGET})

target_link_libraries(
graphene_${CURRENT_TARGET}
graphene::chain_plugin
graphene::p2p
graphene::protocol
graphene_utilities
graphene_time
appbase
)

target_include_directories(graphene_${CURRENT_TARGET}
PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include")

install(TARGETS
graphene_${CURRENT_TARGET}
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#pragma once

#include <appbase/application.hpp>
#include <graphene/plugins/chain/plugin.hpp>
#include <graphene/plugins/p2p/p2p_plugin.hpp>

namespace graphene {
namespace plugins {
namespace witness_guard {

class witness_guard_plugin final
: public appbase::plugin<witness_guard_plugin> {
public:
APPBASE_PLUGIN_REQUIRES(
(graphene::plugins::chain::plugin)
(graphene::plugins::p2p::p2p_plugin)
)

constexpr static const char *plugin_name = "witness_guard";

static const std::string &name() {
static std::string name = plugin_name;
return name;
}

witness_guard_plugin();
~witness_guard_plugin();

void set_program_options(
boost::program_options::options_description &command_line_options,
boost::program_options::options_description &config_file_options
) override;

void plugin_initialize(
const boost::program_options::variables_map &options
) override;

void plugin_startup() override;
void plugin_shutdown() override;

private:
struct impl;
std::unique_ptr<impl> pimpl;
};

} // witness_guard
} // plugins
} // graphene
Loading