Skip to content

Commit 505659c

Browse files
dhyabi2claude
andcommitted
Add storage-weighted proof-of-work policy for state-creating sends
Proof-of-work today is a one-time issuance cost uncorrelated with the permanent storage a block imposes, so minting dust is nearly free. This prices the mass-dust vector in CPU - never a monetary fee, so the feeless model is preserved: a state send whose destination account is not yet opened creates new permanent ledger footprint, so it is required to carry more work than a send to an already-existing account. Legitimate first-contact onboarding pays the higher cost once per real new account (rare, acceptable), while an attacker fanning dust out to fabricated destinations pays it on every block; dodging it by pre-opening the destinations costs an open + receive per destination, which is the self-limiting cost the throttle imposes. block_adds_new_account() detects the state-creating send (destination account absent from the ledger). evaluate_storage_weighted_work() computes the base requirement, scales it by the multiplier when the weight applies (floored at 1.0 so it can only ever raise the bar), and reports whether the block's work satisfies it. This is an advisory policy calculator: it does not change consensus work validation, which would require network-wide activation. - nano/secure/storage_weighted_work.{hpp,cpp}: detection + policy calculator - nano/node/json_handler: "work_storage_weight" RPC (hash, multiplier) - nano/core_test/storage_weighted_work.cpp: new-account weighting, existing- account exemption, and the multiplier floor Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e150a8e commit 505659c

7 files changed

Lines changed: 242 additions & 0 deletions

File tree

nano/core_test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ add_executable(
7272
rep_weight_store.cpp
7373
scheduler_buckets.cpp
7474
state_commitment.cpp
75+
storage_weighted_work.cpp
7576
stats.cpp
7677
signal_manager.cpp
7778
socket.cpp
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#include <nano/lib/blockbuilders.hpp>
2+
#include <nano/lib/blocks.hpp>
3+
#include <nano/lib/numbers.hpp>
4+
#include <nano/lib/work.hpp>
5+
#include <nano/secure/common.hpp>
6+
#include <nano/secure/ledger.hpp>
7+
#include <nano/secure/ledger_set_any.hpp>
8+
#include <nano/secure/storage_weighted_work.hpp>
9+
#include <nano/test_common/ledger_context.hpp>
10+
11+
#include <gtest/gtest.h>
12+
13+
#include <limits>
14+
15+
namespace
16+
{
17+
std::shared_ptr<nano::block> send_to (nano::account const & destination)
18+
{
19+
nano::block_builder builder;
20+
nano::work_pool pool{ nano::dev::network_params.network, std::numeric_limits<unsigned>::max () };
21+
return builder.state ()
22+
.account (nano::dev::genesis_key.pub)
23+
.previous (nano::dev::genesis->hash ())
24+
.representative (nano::dev::genesis_key.pub)
25+
.balance (nano::dev::constants.genesis_amount - 1)
26+
.link (destination)
27+
.sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub)
28+
.work (*pool.generate (nano::dev::genesis->hash ()))
29+
.build ();
30+
}
31+
}
32+
33+
// A send to a not-yet-opened account is storage-weighted: its required work is the base
34+
// requirement scaled by the multiplier, and at multiplier 1 it collapses to the base.
35+
TEST (storage_weighted_work, new_account_is_weighted)
36+
{
37+
auto ctx = nano::test::ledger_empty ();
38+
auto & ledger = ctx.ledger ();
39+
nano::keypair fresh;
40+
auto block = send_to (fresh.pub);
41+
auto transaction = ledger.tx_begin_read ();
42+
43+
ASSERT_TRUE (nano::block_adds_new_account (ledger, transaction, *block));
44+
45+
uint64_t const base = ledger.work.threshold_base (block->work_version ());
46+
auto weighted = nano::evaluate_storage_weighted_work (ledger, transaction, *block, 8.0);
47+
ASSERT_TRUE (weighted.creates_new_account);
48+
ASSERT_EQ (base, weighted.base_threshold);
49+
ASSERT_EQ (nano::difficulty::from_multiplier (8.0, base), weighted.required_threshold);
50+
ASSERT_GT (weighted.required_threshold, weighted.base_threshold);
51+
52+
// Multiplier 1 must not raise the requirement, and the block's own work satisfies it.
53+
auto unweighted = nano::evaluate_storage_weighted_work (ledger, transaction, *block, 1.0);
54+
ASSERT_EQ (base, unweighted.required_threshold);
55+
ASSERT_TRUE (unweighted.satisfies);
56+
}
57+
58+
// A send to an already-opened account carries no storage weight, whatever the multiplier.
59+
TEST (storage_weighted_work, existing_account_not_weighted)
60+
{
61+
auto ctx = nano::test::ledger_empty ();
62+
auto & ledger = ctx.ledger ();
63+
// Genesis account already exists in the ledger.
64+
auto block = send_to (nano::dev::genesis_key.pub);
65+
auto transaction = ledger.tx_begin_read ();
66+
67+
ASSERT_FALSE (nano::block_adds_new_account (ledger, transaction, *block));
68+
69+
auto weighted = nano::evaluate_storage_weighted_work (ledger, transaction, *block, 8.0);
70+
ASSERT_FALSE (weighted.creates_new_account);
71+
ASSERT_EQ (weighted.base_threshold, weighted.required_threshold);
72+
ASSERT_DOUBLE_EQ (1.0, weighted.weight_multiplier);
73+
ASSERT_TRUE (weighted.satisfies);
74+
}
75+
76+
// A multiplier below 1 can never lower the base requirement.
77+
TEST (storage_weighted_work, multiplier_floor)
78+
{
79+
auto ctx = nano::test::ledger_empty ();
80+
auto & ledger = ctx.ledger ();
81+
nano::keypair fresh;
82+
auto block = send_to (fresh.pub);
83+
auto transaction = ledger.tx_begin_read ();
84+
85+
auto result = nano::evaluate_storage_weighted_work (ledger, transaction, *block, 0.25);
86+
ASSERT_EQ (result.base_threshold, result.required_threshold);
87+
ASSERT_DOUBLE_EQ (1.0, result.weight_multiplier);
88+
}

nano/node/json_handler.cpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#include <nano/secure/ledger_set_any.hpp>
3737
#include <nano/secure/ledger_set_cemented.hpp>
3838
#include <nano/secure/state_commitment.hpp>
39+
#include <nano/secure/storage_weighted_work.hpp>
3940
#include <nano/secure/transaction.hpp>
4041
#include <nano/store/ledger/account.hpp>
4142
#include <nano/store/ledger/block.hpp>
@@ -52,6 +53,7 @@
5253

5354
#include <algorithm>
5455
#include <chrono>
56+
#include <cmath>
5557
#include <vector>
5658

5759
namespace
@@ -1810,6 +1812,53 @@ void nano::json_handler::state_pending_sweep ()
18101812
}));
18111813
}
18121814

1815+
void nano::json_handler::work_storage_weight ()
1816+
{
1817+
// Advisory (Block 5): evaluate a block against the storage-weighted PoW requirement.
1818+
// A state send to a not-yet-opened account must carry `multiplier`x the base work.
1819+
auto hash (hash_impl ());
1820+
double multiplier = 8.0; // default: state-creating sends must work 8x harder
1821+
auto multiplier_text (request.get_optional<std::string> ("multiplier"));
1822+
if (!ec && multiplier_text.has_value ())
1823+
{
1824+
try
1825+
{
1826+
multiplier = std::stod (multiplier_text.value ());
1827+
}
1828+
catch (...)
1829+
{
1830+
ec = nano::error_rpc::bad_multiplier_format;
1831+
}
1832+
if (!ec && (multiplier < 1.0 || !std::isfinite (multiplier)))
1833+
{
1834+
ec = nano::error_rpc::bad_multiplier_format;
1835+
}
1836+
}
1837+
if (ec)
1838+
{
1839+
response_errors ();
1840+
return;
1841+
}
1842+
node.workers.post (create_worker_task ([hash, multiplier] (std::shared_ptr<nano::json_handler> const & rpc_l) {
1843+
auto transaction (rpc_l->node.ledger.tx_begin_read ());
1844+
auto block (rpc_l->node.ledger.any.block_get (transaction, hash));
1845+
if (block == nullptr)
1846+
{
1847+
rpc_l->ec = nano::error_blocks::not_found;
1848+
rpc_l->response_errors ();
1849+
return;
1850+
}
1851+
auto result (nano::evaluate_storage_weighted_work (rpc_l->node.ledger, transaction, *block, multiplier));
1852+
rpc_l->response_l.put ("creates_new_account", result.creates_new_account ? "true" : "false");
1853+
rpc_l->response_l.put ("weight_multiplier", nano::to_string (result.weight_multiplier));
1854+
rpc_l->response_l.put ("base_threshold", nano::to_string_hex (result.base_threshold));
1855+
rpc_l->response_l.put ("required_threshold", nano::to_string_hex (result.required_threshold));
1856+
rpc_l->response_l.put ("achieved_difficulty", nano::to_string_hex (result.achieved_difficulty));
1857+
rpc_l->response_l.put ("satisfies", result.satisfies ? "true" : "false");
1858+
rpc_l->response_errors ();
1859+
}));
1860+
}
1861+
18131862
void nano::json_handler::block_create ()
18141863
{
18151864
std::string type (request.get<std::string> ("type"));
@@ -5818,6 +5867,7 @@ ipc_json_handler_no_arg_func_map create_ipc_json_handler_no_arg_func_map ()
58185867
no_arg_funcs.emplace ("state_checkpoint", &nano::json_handler::state_checkpoint);
58195868
no_arg_funcs.emplace ("state_retention_plan", &nano::json_handler::state_retention_plan);
58205869
no_arg_funcs.emplace ("state_pending_sweep", &nano::json_handler::state_pending_sweep);
5870+
no_arg_funcs.emplace ("work_storage_weight", &nano::json_handler::work_storage_weight);
58215871
no_arg_funcs.emplace ("block_create", &nano::json_handler::block_create);
58225872
no_arg_funcs.emplace ("block_hash", &nano::json_handler::block_hash);
58235873
no_arg_funcs.emplace ("bootstrap", &nano::json_handler::bootstrap);

nano/node/json_handler.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class json_handler : public std::enable_shared_from_this<nano::json_handler>
6565
void state_checkpoint ();
6666
void state_retention_plan ();
6767
void state_pending_sweep ();
68+
void work_storage_weight ();
6869
void block_create ();
6970
void block_hash ();
7071
void bootstrap ();

nano/secure/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ add_library(
4242
rep_weights.cpp
4343
state_commitment.hpp
4444
state_commitment.cpp
45+
storage_weighted_work.hpp
46+
storage_weighted_work.cpp
4547
transaction.hpp
4648
voting_policy.hpp
4749
voting_policy.cpp)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#include <nano/lib/blocks.hpp>
2+
#include <nano/lib/constants.hpp>
3+
#include <nano/lib/numbers.hpp>
4+
#include <nano/secure/ledger.hpp>
5+
#include <nano/secure/ledger_set_any.hpp>
6+
#include <nano/secure/storage_weighted_work.hpp>
7+
#include <nano/secure/transaction.hpp>
8+
9+
#include <algorithm>
10+
11+
bool nano::block_adds_new_account (nano::ledger const & ledger, nano::secure::transaction const & transaction, nano::block const & block)
12+
{
13+
if (!block.is_send ())
14+
{
15+
return false;
16+
}
17+
// A state send carries the destination in its link; a legacy send in destination().
18+
nano::account destination = block.link_field ().has_value () ? block.link_field ().value ().as_account () : block.destination ();
19+
// New footprint iff the destination account is not yet opened in the ledger. A dust
20+
// send to a never-opened account satisfies this on every block (the account never
21+
// opens), so each such send pays the elevated cost.
22+
return !ledger.any.account_exists (transaction, destination);
23+
}
24+
25+
nano::storage_weighted_work_result nano::evaluate_storage_weighted_work (nano::ledger const & ledger, nano::secure::transaction const & transaction, nano::block const & block, double new_account_multiplier)
26+
{
27+
nano::storage_weighted_work_result result;
28+
// Never below 1.0: the storage weight only ever raises the requirement.
29+
double const multiplier = std::max (1.0, new_account_multiplier);
30+
31+
result.base_threshold = ledger.work.threshold_base (block.work_version ());
32+
result.achieved_difficulty = ledger.work.difficulty (block);
33+
result.creates_new_account = nano::block_adds_new_account (ledger, transaction, block);
34+
35+
if (result.creates_new_account)
36+
{
37+
result.weight_multiplier = multiplier;
38+
result.required_threshold = nano::difficulty::from_multiplier (multiplier, result.base_threshold);
39+
}
40+
else
41+
{
42+
result.weight_multiplier = 1.0;
43+
result.required_threshold = result.base_threshold;
44+
}
45+
result.satisfies = result.achieved_difficulty >= result.required_threshold;
46+
return result;
47+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#pragma once
2+
3+
#include <nano/lib/numbers.hpp>
4+
5+
#include <cstdint>
6+
7+
namespace nano
8+
{
9+
class block;
10+
class ledger;
11+
namespace secure
12+
{
13+
class transaction;
14+
}
15+
16+
/*
17+
* Block 5 of the ledger-bloat mitigation work: the storage-weighted proof-of-work
18+
* throttle. Proof-of-work today is a one-time issuance cost uncorrelated with the
19+
* PERMANENT storage a block imposes, so minting dust is nearly free. This prices the
20+
* mass-dust vector in CPU (never a monetary fee, so the feeless model is preserved):
21+
* a state send whose destination account is not yet opened creates new permanent
22+
* ledger footprint (a fresh account / receivable that may never be received), so it is
23+
* required to carry more work than a send to an already-existing account.
24+
*
25+
* Legitimate first-contact onboarding pays the higher cost once per real new account -
26+
* rare and acceptable - while an attacker fanning dust out to fabricated destinations
27+
* pays it on every block. An attacker can dodge by pre-opening the destinations they
28+
* control, but that itself costs an open + receive per destination, which is the
29+
* self-limiting cost the throttle is meant to impose.
30+
*
31+
* This is an ADVISORY policy calculator: it computes the storage-weighted requirement
32+
* and whether a block meets it. It does not change consensus work validation, which
33+
* would require network-wide activation.
34+
*/
35+
36+
// True if the block adds a brand-new account to permanent state: a send whose
37+
// destination account is not yet opened in the ledger.
38+
bool block_adds_new_account (nano::ledger const &, nano::secure::transaction const &, nano::block const &);
39+
40+
struct storage_weighted_work_result final
41+
{
42+
bool creates_new_account{ false }; // whether the storage weight applies
43+
double weight_multiplier{ 1.0 }; // multiplier applied to the base requirement
44+
uint64_t base_threshold{ 0 }; // normal work requirement for this block
45+
uint64_t required_threshold{ 0 }; // storage-weighted requirement (>= base_threshold)
46+
uint64_t achieved_difficulty{ 0 }; // difficulty of the block's current work
47+
bool satisfies{ false }; // achieved_difficulty >= required_threshold
48+
};
49+
50+
// Evaluate a block against the storage-weighted requirement. `new_account_multiplier`
51+
// (>= 1.0) is how much harder a state-creating send must work. Advisory / measurement.
52+
nano::storage_weighted_work_result evaluate_storage_weighted_work (nano::ledger const &, nano::secure::transaction const &, nano::block const &, double new_account_multiplier);
53+
}

0 commit comments

Comments
 (0)