Skip to content

Lb/localnet#951

Open
ldmberman wants to merge 82 commits intomasterfrom
lb/localnet
Open

Lb/localnet#951
ldmberman wants to merge 82 commits intomasterfrom
lb/localnet

Conversation

@ldmberman
Copy link
Copy Markdown
Member

@ldmberman ldmberman commented Jan 28, 2026

Note

High Risk
Touches mining/nonce-limiter validation, recall-range computation, storage/rocksdb open semantics, and mempool overspend logic; regressions could impact consensus validation, syncing, or node startup from state snapshots.

Overview
Adds a LOCALNET runtime mode and tooling to run a single-node, snapshot-seeded network. This introduces ar_localnet (start from a snapshot dir, seed data roots/tx data, and create new snapshots) plus a localnet_shell entrypoint and a dedicated ar_localnet_mining_server/supervisor wired via ar_node_worker to support deterministic “mine one block / mine until height” workflows.

Extends “start from state” to accept an explicit start_from_state folder (CLI/config), validates it against data_dir, and updates startup/join logic to read block index/history/wallet state from that folder; related changes include ar_node:read_recent_blocks/3, disk-cache lookups with an optional custom directory, and multiple DB open callsites updated to use the new ar_kv API.

Refactors ar_kv database opening to a map-based open/1 (plus open_readonly/1 and close/1) with explicit paths/log paths and read-only support, then updates storage/sync components accordingly; also adds a synchronous store_data_roots_sync/4 path and a new HTTP POST /data_roots/{offset} endpoint to accept validated data-root indices.

Makes LOCALNET-specific consensus relaxations for testing (e.g., optional precalculated recall ranges, step-count/timeline comparisons, and difficulty checks), and improves mempool overspend filtering by tracking per-origin spent totals (with denomination-aware redenomination) to drop the lowest-priority spends when an origin exceeds confirmed balance.

CI/dev tooling updates include enabling Git LFS on checkouts (and tracking localnet_snapshot/** via LFS), adding headless notebook jobs to the full test workflow, and adding Jupyter config to strip notebook outputs by default.

Written by Cursor Bugbot for commit 94d7166. This will update automatically on new commits. Configure here.

@ldmberman ldmberman force-pushed the lb/localnet branch 2 times, most recently from 3fc613f to 954250a Compare February 4, 2026 12:40
@ldmberman ldmberman force-pushed the lb/localnet branch 2 times, most recently from bfa2e73 to abcce9d Compare February 18, 2026 23:37
Lev Berman added 14 commits March 25, 2026 11:02
mine_one_block was changed from a task queue cast to a synchronous
gen_server:call in a176655 to return {error, mining_server_running}.
This bypassed the task queue's is_joined guard, introducing a race
condition: after ar_test_node:join_on, the gen_server is accepting
calls before the join handler populates the ETS state, so
get_current_diff crashes with badarith on undefined + 1.

Revert mine_one_block to a cast routed through the task queue,
restoring the original design from 96ed594. No caller checks the
return value. The task queue's existing is_joined check naturally
prevents execution before the node state is initialized.
meck's internal gen_server proxy uses gen_server:call/2 with the default
5s timeout, which can fire under CI load. Catch the exit:{timeout, _} so
the do_until loop retries instead of crashing the test. Also bump the
outer do_until timeout from 60s to 120s.
application:stop(arweave) can hang if a supervised process is stuck
(e.g. mid-VDF computation), eating into the eunit test timeout and
failing unrelated tests. Wrap the call with a 60s deadline and
force-kill the application master if it exceeds it.
wait_until_height/4 made two separate calls: do_wait_until_height
(which polls until height >= target) followed by ar_node:get_height().
The node could mine additional blocks between these calls, causing a
strict assertEqual to fail (e.g. expected 14, got 16). Derive the
height from the block index already returned by do_wait_until_height
and use a >= assertion consistent with the wait condition.
Stop application dependencies in reverse order to avoid
crashing dependents (e.g. cowboy) before their dependencies (e.g. ranch).

Also handle {not_started, arweave} in stop/0 which crashes when
clean_up_and_stop is called before arweave has been started.
Add prometheus:start(), arweave_config:start(), and wait_until_joined()
to match the startup sequence used by start_node/3 and prevent the node
from being used before it finishes joining.
Lev Berman added 5 commits March 25, 2026 11:13
The retry-closed-connection change (902b9bc) broadened the error catch
to all {error, _} tuples, unintentionally converting errors like
too_much_data and timeout into client_error.
Start the validator node before the exit node so its HTTP server is available
when peers validate it during startup. This is important when running
tests sequentially via ./bin/test, where a previous test module may have left
arweave stopped on the main node.
StorageModules =
case Config#config.storage_modules of
[] ->
[{21 * ?MiB, 0, {replica_2_9, MiningAddr}}];
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An particular reason for 21 MiB? Or not important?

%% @doc Read recent block heads, tx headers, block time history, and account tree data
%% from the snapshot directory and store them in the data directory. Does not do anything
%% if recent blocks are already available locally.
store_snapshot_data2(SnapshotDir) ->
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a store_snapshot_data should this be renamed to submit_snapshot_data2?

Comment on lines +663 to +673
reward_history_bi(Height, BI) ->
InterimRewardHistoryLength0 = (Height - ar_fork:height_2_8()) + 21600,
InterimRewardHistoryLength =
case InterimRewardHistoryLength0 > 0 of
true ->
InterimRewardHistoryLength0;
false ->
0
end,
RewardHistoryBI0 = ar_rewards:trim_buffered_reward_history(Height, BI),
lists:sublist(RewardHistoryBI0, InterimRewardHistoryLength).
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +216 to +227
%% @doc Format a packing type tuple into a human-readable string for logging.
format_packing({spora_2_6, Addr}) ->
io_lib:format("spora_2_6(~s)", [ar_util:encode(Addr)]);
format_packing({composite, Addr, Diff}) ->
io_lib:format("composite(~s, ~B)", [ar_util:encode(Addr), Diff]);
format_packing({replica_2_9, Addr}) ->
io_lib:format("(replica_2_9, ~s)", [ar_util:encode(Addr)]);
format_packing(unpacked) ->
"unpacked";
format_packing(Other) ->
io_lib:format("~p", [Other]).

Copy link
Copy Markdown
Collaborator

@JamesPiechota JamesPiechota Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible to dedupe with ar_serialize:encode_packing?

%% paths and data roots, register them with ar_data_root_sync, then write the
%% raw transaction data to storage.
%% Return {TotalBigChunkBytes, TotalSmallChunkBytes}.
submit_block_data(BlockStart, TXs) ->
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these submit_xxx functions is the reason we call modules directly rather than going through the "normal" HTTP API for performance reasons? Or maybe because we're loading a snapshot from disk rather than normal syncing the normal API would reject the requests?

%% data included in the localnet snapshot. These are real mainnet transactions
%% whose data is bundled in the snapshot's seed_txs/ directory so the localnet
%% node has chunk data to mine with.
snapshot_txs() ->
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the selected block height special? Or just picked to divide the blockchain into N roughly equal snapshots?

{origin_tx_map, OriginTXMap2}
{origin_tx_map, OriginTXMap2},
{origin_spent_total_map, OriginSpentTotalMap2},
{origin_spent_total_denomination, Denomination}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect it's not really important, but Denomination may still include the impact of TXs that were dropped in the loop above, right? Given how seldom denomination changes (if ever), my guess is this is not relevant in practice. Just trying to confirm my understanding

case Config#config.mine of
true ->
gen_server:cast(?MODULE, automine);
gen_server:cast(?MODULE, start_mining);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
gen_server:cast(?MODULE, start_mining);
start_mining();

-ifdef(LOCALNET).
-define(MINING_SERVER, ar_localnet_mining_server).
-else.
-define(MINING_SERVER, ar_mining_server).
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still see ar_mining_server referenced explicitly in other modules - that okay? i.e. is it only here that we need to swap ar_localnet_mining_server for ar_mining_server?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah it's only when these functions are called that we need to swap things out:

-callback start_mining({DiffPair, MerkleRebaseThreshold, Height}) -> ok when
	DiffPair :: {non_neg_integer() | infinity, non_neg_integer() | infinity},
	MerkleRebaseThreshold :: non_neg_integer() | infinity,
	Height :: non_neg_integer().

-callback pause() -> ok.

-callback is_paused() -> boolean().

-callback set_difficulty(DiffPair :: {non_neg_integer() | infinity, non_neg_integer() | infinity}) -> ok.

-callback set_merkle_rebase_threshold(Threshold :: non_neg_integer() | infinity) -> ok.

-callback set_height(Height :: integer()) -> ok.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants