Bitcoin full node and query server

March 2023

Archive of the libbitcoin Slack #general channel.

Wednesday, March 8, 2023

evoskuil05:27
Back from a few days at a small conf in Jackson Hole. I completed the message broadcaster, which allows protocols to subscribe to messages by type, broadcast internally from other channels. This is an analog to the message distributor, which allows protocols to subscribe to messages by type, sent from the peer of the protocol's channel. This was some backlog I wanted to work off in order to normalize the processing of both types of messages. Both distributor and broadcaster incorporate some optimizations for hashing messages.
👍 1

Thursday, March 9, 2023

evoskuil06:32
Today I added [network].bind = 0.0.0.0:8333 type configuration settings to replace the simple port-only config for inbound connections. The above will bind to all NICs on IPv4, and [network].bind = [::]:8333 will bind all on ipv6. Otherwise individual NICs (including local host) can be explicitly bound. This provides a huge increase in flexibility and was pretty simple - one day implementation and test. This also included a change to the [network].self = 42.42.42.42:8333 setting, which is used to advertised one's own address. This is no a multivalued setting, so that multiple public addresses can be advertised to peers. Note that bound endpoints and public addresses are independent for both security and flexibility purposes.
This should be merged in a few minutes, feel free to try it out. Note that config settings can always be discovered using bn -s.
evoskuil06:48
merged.
pmienk17:00
Topic: Dev Weekly Status Time: March 9, 2023 01:00pm Pacific Time (US and Canada) Join Zoom Meeting https://us05web.zoom.us/j/87666370902?pwd=bEZDUlJ0MGJ5V3dVMUhVdHl5QzdFdz09 Meeting ID: 876 6637 0902 Passcode: 85U0bF
evoskuil20:59
On my way
pmienk21:01
Cool, might be a little rocky. Internet is out here, using my phone. Fingers crossed

Sunday, March 12, 2023

evoskuil10:52
Today I applied the new broadcaster to address message relay between channels. After a few tweaks it's working smoothly. This is a secondary source of incoming messages to a protocol, for which a protocol can register. The other is of course peer traffic. In this case a trickle of message traffic is broadcast to other channels and then sent to peers. The includes the first address message which is unsolicited and sent upon connection as a matter of convention, as a way to advertise one's own address(es). Later the broadcaster will be used for such things as tx forwarding.
(edited)

Wednesday, March 15, 2023

evoskuil07:09
Today I moved fully to node blockchain processing. 🕺🏻
evoskuil07:17
I created the header_in protocol and added it to node sessions using protocol attachment overloading. The store query engine is exposed by protocol base. Used network::message locator heights generation, passed to database::get hashes(heights) and was quickly generating a proper locator sequence (although with no candidate/confirmed blocks these always start at genesis). Set up a SEND(get_headers) and a SUBSCRIBE(headers) and was immediately iterating over the full chain, storing each header. The store is fully concurrent, so need to worry about bouncing store calls to a common strand. Set up a few log messages and watched it run.
No problems, except it was much slower than I expected, regardless of peer.
So I dropped out the database storage, which solved that problem. This led me to realize that I had not bucketized the store - all hashmaps are 100 buckets. Not so great when storing 781,000 blocks, basically linear. :<
So after some quick computations and adding two settings to the config file, I restored the database archival and completed a full header sync against one random peer in 76 seconds, which is well within expectations.
(edited)
evoskuil07:52
The full headers_in protocol, less comments, log messages, and assertions:
void protocol_header_in::start() NOEXCEPT
{
    if (started())
        return;

    SUBSCRIBE_CHANNEL2(headers, handle_receive_headers, _1, _2);
    SEND1(create_get_headers(), handle_send, _1);
    protocol::start();
}

bool protocol_header_in::handle_receive_headers(const code& ec, const headers::cptr& message) NOEXCEPT
{
    if (stopped(ec))
        return false;

    for (const auto& header: message->header_ptrs)
    {
        if (!query().set(*header, database::context{}))
        {
            stop(network::error::protocol_violation);
            return false;
        }
    };

    if (message->header_ptrs.size() == max_get_headers)
    {
        SEND1(create_get_headers({ message->header_ptrs.back()->hash() }), handle_send, _1);
    }

    return true;
}

get_headers protocol_header_in::create_get_headers() NOEXCEPT
{
    return create_get_headers(query().get_hashes(get_blocks::heights(query().get_top_candidate())));
}

get_headers protocol_header_in::create_get_headers(hashes&& hashes) NOEXCEPT
{
    return { std::move(hashes), null_hash };
}
handle_send is a base protocol nop handler.
This simplicity and isolation of protocol implementation is a major design goal of the network stack. Each channel instance operates on its own strand, and all protocol handlers (and protocol::start()) are guaranteed stranded invocation, so they can be coded as if on a single thread. Above is still early, as the headers need to populate an in-memory graph and reorg into the candidate chain based on strength. But the protocol won't be any more complicated.
(edited)
The fact that all queries are now thread safe, and incorporate transactional commit, is also significantly simplifying. There is no need for query completion handing, queries ideally run sequentially. Each channel conversation is ordered on its own strand and can execute concurrently across channels.
(edited)
Header sync performance across peers is presently anecdotal, but I just had a peer show up inbound and complete a full header sync (inbound to the node) in 20 secs, so clearly peer speed is the most limiting factor.

Thursday, March 16, 2023

evoskuil08:33
Did some more work on the headers protocol today. One of the things I wanted to look at was the benefit of obtaining identity hashes from the wire serialization (vs. the header.hash() method, which reserializes the header into the hash accumulator). The tradeoff is memory (caching the hashes) for speed (no need to reserialize to obtain the hash). With headers the expected gain is minimal, since the reserialization is trivial. However, the net difference when applied to the headers message is about 35% reduction in header sync time. I found two very reliable peers, which have fast and reliable times for header sync. They average 14.5 seconds without the caching and 9.5 seconds with it. This implies that the advantage to for applying this to tx hashing should be significant. Even 35% would be huge, but I'd guess we'd be closer to 50% improvement.
(edited)
The implementation was trivial, based on work in system and network previously done in anticipation.
evoskuil08:40
The best I've seen so far, for 781,000 headers, iterating 2,000 at a time from a peer, each validated, looked-up, and stored, is 8 seconds. In years past, on the same computer, with closer to 500,000 blocks, the best we saw was 30-45 secs.
🎉 1
pmienk16:00
Topic: Dev Weekly Status Time: March 16, 2023 01:00pm Pacific Time (US and Canada) Join Zoom Meeting https://us05web.zoom.us/j/87666370902?pwd=bEZDUlJ0MGJ5V3dVMUhVdHl5QzdFdz09 Meeting ID: 876 6637 0902 Passcode: 85U0bF

Saturday, March 18, 2023

evoskuil02:42
Got a few smaller things down yesterday after the meeting. I stubbed in the block and transaction protocols, inbound and outbound. Also resolved a small issue whereby the version protocols need access to the store top, though they are in network not node. Then I installed my new 2TB dedicated blockchain SSD and ran a bunch of tests. Startup and shutdown are clearly much faster than on my HDD, especially with a large address pool. Today I worked on making the logging and console systems simpler and more robust. That's all complete now. The console input can now enable/disable any log level dynamically. Default log levels are initialized with consideration of what log levels may have been compiled out entirely. Events, which use the log system but include no textual messaging, now include a span event, which accepts a starting time_point and diffs that with current time at invocation so that duration of asynchronous operations can be easily logged as events.
👍 1

Monday, March 20, 2023

evoskuil08:47
Implemented basic blocks first sync yesterday. Spent today analyzing it. Overall everything is looking great. From a single peer I downloaded 480,555 blocks, parsed, hashed and stored all headers and txs. I stopped the run today to look at the flush behavior. It took about 15 minutes to flush that full run to my HDD (146.5GB). The network stop is always almost instant, only flushing the logging buffer will slow it down. The store stop is now isolated from that, so we get a clear measure and indication of store flush progress. I'm probably going to add events so that we can reflect progress. There are a lot of tables, and we can get feedback from the msync and fsync completion of each one. That's as good as it will get for the memory maps, but that's enough to know that it isn't dead. The event logging system can be used by a UI to provide progress/feedback for these types of things.
🎉 1
I had one block fail deserialization. This was the result of significant changes I made to the script parser, dealing with the parse/storage of invalid scripts (that may not execute). Patched that and no other problems.
I also looked at the store creation/initialization. It takes 28 secs on my SSD and 49 secs on my HDD to initialize the primary tables. This prob can't be improved. I looked at both std::memset and std::fill for the headers, but they have the same perf. That and flushing the headers to disk is the full cost.
Startup and shutdown with large store files is near instantaneous in the case where there is not a significant amount of new data to flush.
Once we have checkpoints during sync the shutdown will be must faster. The checkpointing will amortize the flush over the sync window.
Overall extremely happy with both performance and reliability.
👍 1

Tuesday, March 21, 2023

evoskuil01:55
I added progress callbacks to the store interface, so --initchain now spits this out:
Please wait while initializing "D:/Source/evoskuil/libbitcoin-node/bin/x64/Release/v143/static/bitcoin/" directory...
create::create_file(header_head)
create::create_file(header_body)
create::create_file(point_head)
create::create_file(point_body)
create::create_file(input_head)
create::create_file(input_body)
create::create_file(output_head)
create::create_file(output_body)
create::create_file(puts_head)
create::create_file(puts_body)
create::create_file(tx_head)
create::create_file(tx_body)
create::create_file(txs_head)
create::create_file(txs_body)
create::create_file(address_head)
create::create_file(address_body)
create::create_file(candidate_head)
create::create_file(candidate_body)
create::create_file(confirmed_head)
create::create_file(confirmed_body)
create::create_file(strong_tx_head)
create::create_file(strong_tx_body)
create::create_file(bootstrap_head)
create::create_file(bootstrap_body)
create::create_file(buffer_head)
create::create_file(buffer_body)
create::create_file(neutrino_head)
create::create_file(neutrino_body)
create::create_file(validated_bk_head)
create::create_file(validated_bk_body)
create::create_file(validated_tx_head)
create::create_file(validated_tx_body)
create::open_file(header_head)
create::open_file(header_body)
create::open_file(point_head)
create::open_file(point_body)
create::open_file(input_head)
create::open_file(input_body)
create::open_file(output_head)
create::open_file(output_body)
create::open_file(puts_head)
create::open_file(puts_body)
create::open_file(tx_head)
create::open_file(tx_body)
create::open_file(txs_head)
create::open_file(txs_body)
create::open_file(address_head)
create::open_file(address_body)
create::open_file(candidate_head)
create::open_file(candidate_body)
create::open_file(confirmed_head)
create::open_file(confirmed_body)
create::open_file(strong_tx_head)
create::open_file(strong_tx_body)
create::open_file(bootstrap_head)
create::open_file(bootstrap_body)
create::open_file(buffer_head)
create::open_file(buffer_body)
create::open_file(neutrino_head)
create::open_file(neutrino_body)
create::open_file(validated_bk_head)
create::open_file(validated_bk_body)
create::open_file(validated_tx_head)
create::open_file(validated_tx_body)
create::load_file(header_head)
create::load_file(header_body)
create::load_file(point_head)
create::load_file(point_body)
create::load_file(input_head)
create::load_file(input_body)
create::load_file(output_head)
create::load_file(output_body)
create::load_file(puts_head)
create::load_file(puts_body)
create::load_file(tx_head)
create::load_file(tx_body)
create::load_file(txs_head)
create::load_file(txs_body)
create::load_file(address_head)
create::load_file(address_body)
create::load_file(candidate_head)
create::load_file(candidate_body)
create::load_file(confirmed_head)
create::load_file(confirmed_body)
create::load_file(strong_tx_head)
create::load_file(strong_tx_body)
create::load_file(bootstrap_head)
create::load_file(bootstrap_body)
create::load_file(buffer_head)
create::load_file(buffer_body)
create::load_file(neutrino_head)
create::load_file(neutrino_body)
create::load_file(validated_bk_head)
create::load_file(validated_bk_body)
create::load_file(validated_tx_head)
create::load_file(validated_tx_body)
create::create_table(header_table)
create::create_table(point_table)
create::create_table(input_table)
create::create_table(output_table)
create::create_table(puts_table)
create::create_table(tx_table)
create::create_table(txs_table)
create::create_table(address_table)
create::create_table(candidate_table)
create::create_table(confirmed_table)
create::create_table(strong_tx_table)
create::create_table(bootstrap_table)
create::create_table(buffer_table)
create::create_table(neutrino_table)
create::create_table(validated_bk_table)
create::create_table(validated_tx_table)
create::unload_file(header_head)
create::unload_file(header_body)
create::unload_file(point_head)
create::unload_file(point_body)
create::unload_file(input_head)
create::unload_file(input_body)
create::unload_file(output_head)
create::unload_file(output_body)
create::unload_file(puts_head)
create::unload_file(puts_body)
create::unload_file(tx_head)
create::unload_file(tx_body)
create::unload_file(txs_head)
create::unload_file(txs_body)
create::unload_file(address_head)
create::unload_file(address_body)
create::unload_file(candidate_head)
create::unload_file(candidate_body)
create::unload_file(confirmed_head)
create::unload_file(confirmed_body)
create::unload_file(strong_tx_head)
create::unload_file(strong_tx_body)
create::unload_file(bootstrap_head)
create::unload_file(bootstrap_body)
create::unload_file(buffer_head)
create::unload_file(buffer_body)
create::unload_file(neutrino_head)
create::unload_file(neutrino_body)
create::unload_file(validated_bk_head)
create::unload_file(validated_bk_body)
create::unload_file(validated_tx_head)
create::unload_file(validated_tx_body)
create::close_file(header_head)
create::close_file(header_body)
create::close_file(point_head)
create::close_file(point_body)
create::close_file(input_head)
create::close_file(input_body)
create::close_file(output_head)
create::close_file(output_body)
create::close_file(puts_head)
create::close_file(puts_body)
create::close_file(tx_head)
create::close_file(tx_body)
create::close_file(txs_head)
create::close_file(txs_body)
create::close_file(address_head)
create::close_file(address_body)
create::close_file(candidate_head)
create::close_file(candidate_body)
create::close_file(confirmed_head)
create::close_file(confirmed_body)
create::close_file(strong_tx_head)
create::close_file(strong_tx_body)
create::close_file(bootstrap_head)
create::close_file(bootstrap_body)
create::close_file(buffer_head)
create::close_file(buffer_body)
create::close_file(neutrino_head)
create::close_file(neutrino_body)
create::close_file(validated_bk_head)
create::close_file(validated_bk_body)
create::close_file(validated_tx_head)
create::close_file(validated_tx_body)
open::open_file(header_head)
open::open_file(header_body)
open::open_file(point_head)
open::open_file(point_body)
open::open_file(input_head)
open::open_file(input_body)
open::open_file(output_head)
open::open_file(output_body)
open::open_file(puts_head)
open::open_file(puts_body)
open::open_file(tx_head)
open::open_file(tx_body)
open::open_file(txs_head)
open::open_file(txs_body)
open::open_file(address_head)
open::open_file(address_body)
open::open_file(candidate_head)
open::open_file(candidate_body)
open::open_file(confirmed_head)
open::open_file(confirmed_body)
open::open_file(strong_tx_head)
open::open_file(strong_tx_body)
open::open_file(bootstrap_head)
open::open_file(bootstrap_body)
open::open_file(buffer_head)
open::open_file(buffer_body)
open::open_file(neutrino_head)
open::open_file(neutrino_body)
open::open_file(validated_bk_head)
open::open_file(validated_bk_body)
open::open_file(validated_tx_head)
open::open_file(validated_tx_body)
open::load_file(header_head)
open::load_file(header_body)
open::load_file(point_head)
open::load_file(point_body)
open::load_file(input_head)
open::load_file(input_body)
open::load_file(output_head)
open::load_file(output_body)
open::load_file(puts_head)
open::load_file(puts_body)
open::load_file(tx_head)
open::load_file(tx_body)
open::load_file(txs_head)
open::load_file(txs_body)
open::load_file(address_head)
open::load_file(address_body)
open::load_file(candidate_head)
open::load_file(candidate_body)
open::load_file(confirmed_head)
open::load_file(confirmed_body)
open::load_file(strong_tx_head)
open::load_file(strong_tx_body)
open::load_file(bootstrap_head)
open::load_file(bootstrap_body)
open::load_file(buffer_head)
open::load_file(buffer_body)
open::load_file(neutrino_head)
open::load_file(neutrino_body)
open::load_file(validated_bk_head)
open::load_file(validated_bk_body)
open::load_file(validated_tx_head)
open::load_file(validated_tx_body)
open::verify_table(header_table)
open::verify_table(point_table)
open::verify_table(input_table)
open::verify_table(output_table)
open::verify_table(puts_table)
open::verify_table(tx_table)
open::verify_table(txs_table)
open::verify_table(address_table)
open::verify_table(candidate_table)
open::verify_table(confirmed_table)
open::verify_table(strong_tx_table)
open::verify_table(bootstrap_table)
open::verify_table(buffer_table)
open::verify_table(neutrino_table)
open::verify_table(validated_bk_table)
open::verify_table(validated_tx_table)
initialize::genesis_block
close::close_table(header_table)
close::close_table(point_table)
close::close_table(input_table)
close::close_table(output_table)
close::close_table(puts_table)
close::close_table(tx_table)
close::close_table(txs_table)
close::close_table(address_table)
close::close_table(candidate_table)
close::close_table(confirmed_table)
close::close_table(strong_tx_table)
close::close_table(bootstrap_table)
close::close_table(buffer_table)
close::close_table(neutrino_table)
close::close_table(validated_bk_table)
close::close_table(validated_tx_table)
close::unload_file(header_head)
close::unload_file(header_body)
close::unload_file(point_head)
close::unload_file(point_body)
close::unload_file(input_head)
close::unload_file(input_body)
close::unload_file(output_head)
close::unload_file(output_body)
close::unload_file(puts_head)
close::unload_file(puts_body)
close::unload_file(tx_head)
close::unload_file(tx_body)
close::unload_file(txs_head)
close::unload_file(txs_body)
close::unload_file(address_head)
close::unload_file(address_body)
close::unload_file(candidate_head)
close::unload_file(candidate_body)
close::unload_file(confirmed_head)
close::unload_file(confirmed_body)
close::unload_file(strong_tx_head)
close::unload_file(strong_tx_body)
close::unload_file(bootstrap_head)
close::unload_file(bootstrap_body)
close::unload_file(buffer_head)
close::unload_file(buffer_body)
close::unload_file(neutrino_head)
close::unload_file(neutrino_body)
close::unload_file(validated_bk_head)
close::unload_file(validated_bk_body)
close::unload_file(validated_tx_head)
close::unload_file(validated_tx_body)
close::close_file(header_head)
close::close_file(header_body)
close::close_file(point_head)
close::close_file(point_body)
close::close_file(input_head)
close::close_file(input_body)
close::close_file(output_head)
close::close_file(output_body)
close::close_file(puts_head)
close::close_file(puts_body)
close::close_file(tx_head)
close::close_file(tx_body)
close::close_file(txs_head)
close::close_file(txs_body)
close::close_file(address_head)
close::close_file(address_body)
close::close_file(candidate_head)
close::close_file(candidate_body)
close::close_file(confirmed_head)
close::close_file(confirmed_body)
close::close_file(strong_tx_head)
close::close_file(strong_tx_body)
close::close_file(bootstrap_head)
close::close_file(bootstrap_body)
close::close_file(buffer_head)
close::close_file(buffer_body)
close::close_file(neutrino_head)
close::close_file(neutrino_body)
close::close_file(validated_bk_head)
close::close_file(validated_bk_body)
close::close_file(validated_tx_head)
close::close_file(validated_tx_body)
Completed initialization.
And startup and shutdown...
> bn -c bn.cfg
2023-03-21T01:55:24Z.0 ====================== startup =======================
2023-03-21T01:55:24Z.0 Using config file: "bn.cfg"
2023-03-21T01:55:24Z.0 Press CTRL-C to stop the node.
2023-03-21T01:55:24Z.0 Please wait while the store is starting...
2023-03-21T01:55:24Z.0 open::open_file(header_head)
2023-03-21T01:55:24Z.0 open::open_file(header_body)
2023-03-21T01:55:24Z.0 open::open_file(point_head)
2023-03-21T01:55:24Z.0 open::open_file(point_body)
2023-03-21T01:55:24Z.0 open::open_file(input_head)
2023-03-21T01:55:24Z.0 open::open_file(input_body)
2023-03-21T01:55:24Z.0 open::open_file(output_head)
2023-03-21T01:55:24Z.0 open::open_file(output_body)
2023-03-21T01:55:24Z.0 open::open_file(puts_head)
2023-03-21T01:55:24Z.0 open::open_file(puts_body)
2023-03-21T01:55:24Z.0 open::open_file(tx_head)
2023-03-21T01:55:24Z.0 open::open_file(tx_body)
2023-03-21T01:55:24Z.0 open::open_file(txs_head)
2023-03-21T01:55:24Z.0 open::open_file(txs_body)
2023-03-21T01:55:24Z.0 open::open_file(address_head)
2023-03-21T01:55:24Z.0 open::open_file(address_body)
2023-03-21T01:55:24Z.0 open::open_file(candidate_head)
2023-03-21T01:55:24Z.0 open::open_file(candidate_body)
2023-03-21T01:55:24Z.0 open::open_file(confirmed_head)
2023-03-21T01:55:24Z.0 open::open_file(confirmed_body)
2023-03-21T01:55:24Z.0 open::open_file(strong_tx_head)
2023-03-21T01:55:24Z.0 open::open_file(strong_tx_body)
2023-03-21T01:55:24Z.0 open::open_file(bootstrap_head)
2023-03-21T01:55:24Z.0 open::open_file(bootstrap_body)
2023-03-21T01:55:24Z.0 open::open_file(buffer_head)
2023-03-21T01:55:24Z.0 open::open_file(buffer_body)
2023-03-21T01:55:24Z.0 open::open_file(neutrino_head)
2023-03-21T01:55:24Z.0 open::open_file(neutrino_body)
2023-03-21T01:55:24Z.0 open::open_file(validated_bk_head)
2023-03-21T01:55:24Z.0 open::open_file(validated_bk_body)
2023-03-21T01:55:24Z.0 open::open_file(validated_tx_head)
2023-03-21T01:55:24Z.0 open::open_file(validated_tx_body)
2023-03-21T01:55:24Z.0 open::load_file(header_head)
2023-03-21T01:55:24Z.0 open::load_file(header_body)
2023-03-21T01:55:24Z.0 open::load_file(point_head)
2023-03-21T01:55:24Z.0 open::load_file(point_body)
2023-03-21T01:55:24Z.0 open::load_file(input_head)
2023-03-21T01:55:24Z.0 open::load_file(input_body)
2023-03-21T01:55:24Z.0 open::load_file(output_head)
2023-03-21T01:55:24Z.0 open::load_file(output_body)
2023-03-21T01:55:24Z.0 open::load_file(puts_head)
2023-03-21T01:55:24Z.0 open::load_file(puts_body)
2023-03-21T01:55:24Z.0 open::load_file(tx_head)
2023-03-21T01:55:24Z.0 open::load_file(tx_body)
2023-03-21T01:55:24Z.0 open::load_file(txs_head)
2023-03-21T01:55:24Z.0 open::load_file(txs_body)
2023-03-21T01:55:24Z.0 open::load_file(address_head)
2023-03-21T01:55:24Z.0 open::load_file(address_body)
2023-03-21T01:55:24Z.0 open::load_file(candidate_head)
2023-03-21T01:55:24Z.0 open::load_file(candidate_body)
2023-03-21T01:55:24Z.0 open::load_file(confirmed_head)
2023-03-21T01:55:24Z.0 open::load_file(confirmed_body)
2023-03-21T01:55:24Z.0 open::load_file(strong_tx_head)
2023-03-21T01:55:24Z.0 open::load_file(strong_tx_body)
2023-03-21T01:55:24Z.0 open::load_file(bootstrap_head)
2023-03-21T01:55:24Z.0 open::load_file(bootstrap_body)
2023-03-21T01:55:24Z.0 open::load_file(buffer_head)
2023-03-21T01:55:24Z.0 open::load_file(buffer_body)
2023-03-21T01:55:24Z.0 open::load_file(neutrino_head)
2023-03-21T01:55:24Z.0 open::load_file(neutrino_body)
2023-03-21T01:55:24Z.0 open::load_file(validated_bk_head)
2023-03-21T01:55:24Z.0 open::load_file(validated_bk_body)
2023-03-21T01:55:24Z.0 open::load_file(validated_tx_head)
2023-03-21T01:55:24Z.0 open::load_file(validated_tx_body)
2023-03-21T01:55:24Z.0 open::verify_table(header_table)
2023-03-21T01:55:24Z.0 open::verify_table(point_table)
2023-03-21T01:55:24Z.0 open::verify_table(input_table)
2023-03-21T01:55:24Z.0 open::verify_table(output_table)
2023-03-21T01:55:24Z.0 open::verify_table(puts_table)
2023-03-21T01:55:24Z.0 open::verify_table(tx_table)
2023-03-21T01:55:24Z.0 open::verify_table(txs_table)
2023-03-21T01:55:24Z.0 open::verify_table(address_table)
2023-03-21T01:55:24Z.0 open::verify_table(candidate_table)
2023-03-21T01:55:24Z.0 open::verify_table(confirmed_table)
2023-03-21T01:55:24Z.0 open::verify_table(strong_tx_table)
2023-03-21T01:55:24Z.0 open::verify_table(bootstrap_table)
2023-03-21T01:55:24Z.0 open::verify_table(buffer_table)
2023-03-21T01:55:24Z.0 open::verify_table(neutrino_table)
2023-03-21T01:55:24Z.0 open::verify_table(validated_bk_table)
2023-03-21T01:55:24Z.0 open::verify_table(validated_tx_table)
2023-03-21T01:55:24Z.0 Please wait while the network is starting...
2023-03-21T01:55:24Z.1 Loaded (10000) addresses.
2023-03-21T01:55:24Z.1 Bypassed seeding because outbound connections disabled.
2023-03-21T01:55:24Z.0 Node is started.
2023-03-21T01:55:24Z.1 Maintaining manual connection to [35.197.69.139:8333]
2023-03-21T01:55:24Z.1 Not configured for inbound connections.
2023-03-21T01:55:24Z.1 Not configured for outbound connections.
2023-03-21T01:55:24Z.0 Node is running.
2023-03-21T01:55:24Z.1 Outbound [35.197.69.139:8333] version (70016) /Satoshi:24.0.1/
2023-03-21T01:55:24Z.3 Manual channel start [35.197.69.139:8333] success
2023-03-21T01:55:27Z.0 CONSOLE: 'q'
2023-03-21T01:55:27Z.0 Please wait while the network is stopping...
2023-03-21T01:55:27Z.3 Manual channel stop [35.197.69.139:8333] service stopped
2023-03-21T01:55:27Z.1 Saved (10000) addresses.
2023-03-21T01:55:27Z.0 Please wait while the store is stopping...
2023-03-21T01:55:27Z.0 close::close_table(header_table)
2023-03-21T01:55:27Z.0 close::close_table(point_table)
2023-03-21T01:55:27Z.0 close::close_table(input_table)
2023-03-21T01:55:27Z.0 close::close_table(output_table)
2023-03-21T01:55:27Z.0 close::close_table(puts_table)
2023-03-21T01:55:27Z.0 close::close_table(tx_table)
2023-03-21T01:55:27Z.0 close::close_table(txs_table)
2023-03-21T01:55:27Z.0 close::close_table(address_table)
2023-03-21T01:55:27Z.0 close::close_table(candidate_table)
2023-03-21T01:55:27Z.0 close::close_table(confirmed_table)
2023-03-21T01:55:27Z.0 close::close_table(strong_tx_table)
2023-03-21T01:55:27Z.0 close::close_table(bootstrap_table)
2023-03-21T01:55:27Z.0 close::close_table(buffer_table)
2023-03-21T01:55:27Z.0 close::close_table(neutrino_table)
2023-03-21T01:55:27Z.0 close::close_table(validated_bk_table)
2023-03-21T01:55:27Z.0 close::close_table(validated_tx_table)
2023-03-21T01:55:27Z.0 close::unload_file(header_head)
2023-03-21T01:55:27Z.0 close::unload_file(header_body)
2023-03-21T01:55:27Z.0 close::unload_file(point_head)
2023-03-21T01:55:28Z.0 close::unload_file(point_body)
2023-03-21T01:55:28Z.0 close::unload_file(input_head)
2023-03-21T01:55:29Z.0 close::unload_file(input_body)
2023-03-21T01:55:29Z.0 close::unload_file(output_head)
2023-03-21T01:55:29Z.0 close::unload_file(output_body)
2023-03-21T01:55:29Z.0 close::unload_file(puts_head)
2023-03-21T01:55:29Z.0 close::unload_file(puts_body)
2023-03-21T01:55:29Z.0 close::unload_file(tx_head)
2023-03-21T01:55:35Z.0 close::unload_file(tx_body)
2023-03-21T01:55:35Z.0 close::unload_file(txs_head)
2023-03-21T01:55:35Z.0 close::unload_file(txs_body)
2023-03-21T01:55:35Z.0 close::unload_file(address_head)
2023-03-21T01:55:35Z.0 close::unload_file(address_body)
2023-03-21T01:55:35Z.0 close::unload_file(candidate_head)
2023-03-21T01:55:35Z.0 close::unload_file(candidate_body)
2023-03-21T01:55:35Z.0 close::unload_file(confirmed_head)
2023-03-21T01:55:35Z.0 close::unload_file(confirmed_body)
2023-03-21T01:55:35Z.0 close::unload_file(strong_tx_head)
2023-03-21T01:55:35Z.0 close::unload_file(strong_tx_body)
2023-03-21T01:55:35Z.0 close::unload_file(bootstrap_head)
2023-03-21T01:55:35Z.0 close::unload_file(bootstrap_body)
2023-03-21T01:55:35Z.0 close::unload_file(buffer_head)
2023-03-21T01:55:35Z.0 close::unload_file(buffer_body)
2023-03-21T01:55:35Z.0 close::unload_file(neutrino_head)
2023-03-21T01:55:35Z.0 close::unload_file(neutrino_body)
2023-03-21T01:55:35Z.0 close::unload_file(validated_bk_head)
2023-03-21T01:55:35Z.0 close::unload_file(validated_bk_body)
2023-03-21T01:55:35Z.0 close::unload_file(validated_tx_head)
2023-03-21T01:55:35Z.0 close::unload_file(validated_tx_body)
2023-03-21T01:55:35Z.0 close::close_file(header_head)
2023-03-21T01:55:35Z.0 close::close_file(header_body)
2023-03-21T01:55:35Z.0 close::close_file(point_head)
2023-03-21T01:55:35Z.0 close::close_file(point_body)
2023-03-21T01:55:35Z.0 close::close_file(input_head)
2023-03-21T01:55:35Z.0 close::close_file(input_body)
2023-03-21T01:55:35Z.0 close::close_file(output_head)
2023-03-21T01:55:35Z.0 close::close_file(output_body)
2023-03-21T01:55:35Z.0 close::close_file(puts_head)
2023-03-21T01:55:35Z.0 close::close_file(puts_body)
2023-03-21T01:55:35Z.0 close::close_file(tx_head)
2023-03-21T01:55:35Z.0 close::close_file(tx_body)
2023-03-21T01:55:35Z.0 close::close_file(txs_head)
2023-03-21T01:55:35Z.0 close::close_file(txs_body)
2023-03-21T01:55:35Z.0 close::close_file(address_head)
2023-03-21T01:55:35Z.0 close::close_file(address_body)
2023-03-21T01:55:35Z.0 close::close_file(candidate_head)
2023-03-21T01:55:35Z.0 close::close_file(candidate_body)
2023-03-21T01:55:35Z.0 close::close_file(confirmed_head)
2023-03-21T01:55:35Z.0 close::close_file(confirmed_body)
2023-03-21T01:55:35Z.0 close::close_file(strong_tx_head)
2023-03-21T01:55:35Z.0 close::close_file(strong_tx_body)
2023-03-21T01:55:35Z.0 close::close_file(bootstrap_head)
2023-03-21T01:55:35Z.0 close::close_file(bootstrap_body)
2023-03-21T01:55:35Z.0 close::close_file(buffer_head)
2023-03-21T01:55:35Z.0 close::close_file(buffer_body)
2023-03-21T01:55:35Z.0 close::close_file(neutrino_head)
2023-03-21T01:55:35Z.0 close::close_file(neutrino_body)
2023-03-21T01:55:35Z.0 close::close_file(validated_bk_head)
2023-03-21T01:55:35Z.0 close::close_file(validated_bk_body)
2023-03-21T01:55:35Z.0 close::close_file(validated_tx_head)
2023-03-21T01:55:35Z.0 close::close_file(validated_tx_body)
2023-03-21T01:55:35Z.0 Node stopped successfully.
2023-03-21T01:55:35Z.0 ====================== shutdown ======================
Notice that some lines have material delays, such as this [6 secs]:
2023-03-21T01:55:29Z.0 close::unload_file(tx_head)
This is a major improvement when it comes to managing the shutdown delay from flushing, and can be trivially integrated into graphical interface progress feedback.
🎉 1
evoskuil02:10
The above 6 sec delay is the result of actually loading a significant number of blocks during the 3 seconds that the node was running. Startup is always less than 1 sec, even with a large store. With nothing new to flush, shutdown is < 1 sec with a new store, and on my HDD is 10 secs with my current 138GB store.
(edited)
I haven't yet explored parallelization of shutdowns. This is all presently sequential and operates on the main node application thread before network start and after network stop. Isolating the store from the network greatly simplifies things, including mock store injection.
(edited)
evoskuil02:48
I just tested the same 138GB store on SSD, and with nothing to flush that unloads in 6 seconds (vs. 10).
evoskuil04:11
I've now got a basic naïve block sync going for multiple concurrent peers. Apart from lack of validation it's basically the way v3 works. Nice to see that 10 arbitrary concurrent peers syncing blocks-first has about the same block rate a single fast peer. CPU load is balancing nicely across peers and memory is being held very constant over time by the OS.
image.png
The node remains entirely free of any shutdown hangs, crashes, or other unexplained behavior.
evoskuil04:20
10 threads allocated/busy for network, 1 dedicated/busy logging, 1 idle for console input, 1 idle for stop handling (and 3 owned by windows).
image.png
evoskuil04:26
This is of course just baselining for the target node architecture. Making sure network is sold, deserialization is correct across the full chain, table and header sizings are optimal, store concurrency is reliable and performant, logging is flexible and helpful.
👍 1

Wednesday, March 22, 2023

evoskuil00:32
Completed a full sync against a single peer. This verifies the database sizing calculations, all chain object deserializations, and full sync shutdown cost. This was a single run for the whole chain against a single peer. That provides a good baseline for RAM usage for the memory maps. My machine held the process private bytes to almost exactly 1GB. This flushed to disk in 5 minutes, with cost-per-file appearing roughly proportional to file size. The final store size, headers and bodies, at 781,877 blocks is 426.73GB.
evoskuil00:37
This store is the full chain, including full constant time indexation. But it does not include validation/confirmation metadata. My original calcs show that at about 46GB. This would put current sizing at 473GB. Current core sizing is reported at 456.82GB. Note that Core does not provide full indexation at this size (eg no tx index).
evoskuil00:42
However, after taking into account the 10.2% growth in the chain since I did my calcs a year ago, the project size (without metadata) would be 477GB, but has actually come in at 427GB. This is due to some further space optimization opportunity I found during implementation. So we are 50GB smaller than anticipated. This may carry over into the metadata as well, leaving the store very close to Core sizing - despite far greater indexation.
🎉 1
evoskuil01:17
I initially estimated table sizes to preallocate the full archive, and got pretty close in most cases. But inputs was way low and outputs was way high. So it looks like the key assumption of input/output ratio was off. I'm going to write a tool to scan the entire store of blocks to obtain the exact count of inputs and outputs, and the total wire serialization size of the store. By verifying merkle trees that can also ensure that serialization is fully round-tripping.
Hash table collision average is similarly very close to expected, and off for input for the same reason (output isn't a hash table). By obtaining the input count I can tune this closely. I've generally targeted a collision rate of 1.5. This run achieved 1.65, 1.77, 1.79. The next run will be able to hit 1.5 precisely.
evoskuil03:23
In the first run I encountered a simple script deserialization failure, which is resolved. In the second run I encountered a link overflow. This was due to running multiple peers in a race for blocks. Since there is not yet a block chaser to eliminate redundancies, the store had a large amount of redundant writes (stopped at ~800GB at ~block 600,000). These become orphaned naturally and are not technically a problem. However the tables use byte-aligned internal links that are sized based on expected table size. This run overflowed the txs association table link (4 bytes), effectively killing the run. I hadn't added a runtime guard in for this scenario, but I figure it's worth the trivial extra check. So that's included now. The node will encounter a handled write failure in the case of link type overflow, and this implies that the node must be recompiled using a larger link capacity for that table. In v3 links were all either 32 or 64 bytes, which had the same issue but also wasted a huge amount of space. Presently we use 3/4/5 byte link sizes. The third run (with one peer, so no redundancies) completed as expected. In analyzing sizes I noticed that the both this run and the previous overflowed the puts association table link. But in this case there would be no failure unless a read of the txs is attempted. In the case of txs there was a read in order to test for block existence, which caused the halt. The reason this run overflowed puts is that my estimate of inputs/outputs count was too low. As configured it was targeting 78% capacity, but it hit 104%. This means the link type for puts must be increased from 4 bytes to 5. That implies a .7GB store increase. All link types now have cushion in bitcoin for many years to come.
(edited)
Hash table header bucket count is also an important performance factor. This is set at first run time vs. compile time. Any change to the bucket count requires a store resync, but going from 1.5 to 2.0 collision rate requires a few years.
evoskuil12:22
I completed the increase to the puts association table link from 4 to 5 bytes. Also added a new bn command line option bn --totals to count up inputs and outputs in the store. These are slabs storage (variable length, because of script/witness data) and therefore their count cannot be inferred from the table size (unlike the other tables). This scans at a rate of about 1 million txs per second, so will take about 13 minutes to scan the entire store once populated. I'll resync tonight and with the new counts will be able to complete the intended default sizing configuration for header buckets at 1.5 collision rate and body initial sizes to preclude resizing.

Thursday, March 23, 2023

evoskuil04:09
Completed the sync with increased txs link size. As expected, the store grew by .78GB, not including growth for intervening blocks. With proper byte sizing and record counts for all archive tables I've updated default config for file start sizes and hash table header bucket counts (targeting has collision rate of 1.5) and set up another run. The inputs/outputs count scan takes 15 minutes, as expected. I incorporated emission to console of important values on the --initchain and --totals command line tools, as well as emission to log for node execution. The naive sync against a single peer takes about 11 hours. Will run another tonight against the updated store sizing. Encountered no problems on the last run. I did notice that the txs table link sizing is at 76% capacity, so should probably be updated to 5 bytes before the end of the year. Also simple and will be about a 1GB increase. Other tables are all below 20% capacity.
evoskuil04:28
So now that basic headers and blocks sync and storage is verified live, and tools are in good shape, I'll soon be moving on to the header chaser and block chaser. The header chaser maintains the pre-candidate header tree, validating and committing headers as they create a strongest and recent branch. The block chaser manages the parallel block download window and peer speed. But first I'm going to spend a day on optimizing block.tx hash computations from the wire serialization. Most of that work was already done, and it's been applied to headers and block.header, just not block.tx. This should significantly reduce deserialization/storage time, which will be easy to measure now against this baseline run.
👍 1
evoskuil19:33
Bucket sizes with collision rates now:
   header :521357 (1.50029)
   txs    :521357 (1.50029)
   tx     :544452363 (1.50038)
   point  :539868003 (1.50037)
   input  :1440664028 (1.50038)
🎉 1

Friday, March 24, 2023

evoskuil10:59
image.png
evoskuil11:05
Some initial log-to-graph results... I completed the implementation of tx hash computation from the message buffer, which doesn't appear to be a material gain. Also discovered I wasn't requesting witness blocks, so updated that. This plot shows a few metrics at 10k block intervals up to 300k. Time and tx count are scaled for presentation. It appears that in this range at least block download, parse, hash and archive (time) is linear relative to the amount of data (archive [total store], input, output, put [input + output] sizes), and objects (tx count) in the store. Each of these lines very closely overlays the other with a constant scale factor. I'm doing a full chain run now to verify that this holds for the entire chain. This is all still just a single peer connection with sequential block download (against a pretty consistent peer).
(edited)
evoskuil20:12
image.png
While this initially looks parabolic, that's just going from empty to full blocks, which starts at 200k and ends at 400k. Interesting that input size bends down around 500k, bringing total puts and archive with it, while tx count and outputs stays constant. The important observation is that time is linear in relation to archive size.
evoskuil21:39
This one demonstrates the linearity, and includes the full chain...
evoskuil21:46
The first 100k blocks are cropped, as they throw off the scale. These are ratios of total sync time by various store sizings at that time. Notice that once blocks fill up these remain essentially constant. Note that there is a slight inflection at around 480k. That is the point of segwit activation. At this point time/input (which includes witness) increases while time/output decreases. The net matches the time/archive, which makes sense since the archive is mostly inputs + outputs. It's not entirely clear to me why there remains a slight positive trend after segwit. This implies a slight non-linearity in cost. But obviously it is not a material issue, just a curiosity.
1 reply · evoskuil · last Mar 24, 2023
evoskuil22:17
"This implies a slight non-linearity in cost." -> should say "non-constancy".
evoskuil21:47
This is the corresponding graph of absolutes...
image.png
Until there is a little more history it should be taken with a grain of salt. All time costs are relative to my internet performance, that of the single peer, and its node performance (though I picked one that appears very consistent on about 10 full runs).
(edited)
evoskuil21:59
Some observations regarding the memory map. My machine is 5 years old, has 256GB RAM, and a new 2TB SSD. As the run was completing I brought up the performance monitor and saw clearly that 255 GB of RAM was allocated to the process. There was a very consistent/flat level of reads that were roughly equal to the amount of writes, though the writes were very spikey. The reads are are expected to hit the hash table headers and walk hash deconfliction linked lists within the files, at arbitrary positions. Given that the store is close to twice my available RAM, this requires a material and very consistent level of memory map paging. This type of store is optimized for a machine with sufficient RAM available to hold the entire blockchain. That was the case when I bought this machine. My mac pro has 1TB RAM, which should make for some good stats. This store ideally targets NVRAM, eliminating paging altogether. But the interface is relational, allowing it to sit on a traditional SQL interface. As the sync completed I watched the SSD go to 100% and the RAM profile ramp down in opposition, until SSD went to 0% and RAM was at about 10%. This was the OS automatically unloading the pages that were no longer in use - as expected.
(edited)
evoskuil22:05
Shutdown at that point took 2 mins and 22 secs to unload the memory maps. Not bad at all given that this was a full sync. The final store was 400GB (430,303,292,103 bytes).

Saturday, March 25, 2023

evoskuil00:54
Well, I just figured out the likely reason for the input size deflection at segwit, I'm not getting witness-included blocks over the wire. This was nothing more than a config setting that I forgot to update (minimum required services, witness bit). This also explains why the store size came in well below estimate. The optimizations I made in implementation aren't enough to account for the savings. This will increase the size of the base store, and should bring the yellow and red (size of inputs, size of total puts) into the same slope as the others. Not sure what it will do with proportionality, probably not much - as both bytes and time will increase. Though there is proportionally less computation in just serializing more data into the same number of inputs, so it may flatten them out even more. Will run tonight and find out tomorrow afternoon.
👍 1
7 replies · neillm, evoskuil · last Mar 25, 2023
neillm02:46
V3 nodes storage reqs are currently near ~850GB, so ... when mainnet ready?! 🤪
neillm02:48
And amazing work for sure, big congrats! 🎉 This is certainly the kind of underappreciated work that has amazing long term impact 😄
evoskuil04:05
Thanks! I'm hopeful. The 2 years worth of work prior to getting the node running are paying off 🙂.
🎉 1
evoskuil04:11
I'm estimating about 430GB for the base store, and about 480GB with all validation/confirmation metadata. And with address indexation, around 500GB. Still better than 850 🙂.
neillm04:12
Awesome! Yes, way better!! 🎉
evoskuil04:12
But way better in a lot of ways.
evoskuil04:14
The native data is fully indexed for constant time navigation in all directions. Address index will capture all inputs/outputs using the Electrum addressing index mechanism. Read and write should be significantly faster and less disk intensive, with append only data tables.
👍 1
evoskuil20:48
Full run with witness blocks completed. This run hashes only the block headers and each transaction using the legacy hash function (witnessed hash is different). These are used for index keying. Measured at block 780,000... The run took an extra 15 minutes (10.74 vs. 10.48) and the store was byte size and object count identical to the previous run until the first witness block, at which point input, puts and total archive sizes all increase, as expected. Store size increased from 388 to 487GB (+99GB). That was a lot more for witness data than I expected, and has accelerated. The run did however remove the inflection in the graph that started at segwit...
This is the latest run, with constant scale factors on txs (count) and time (secs).
image.png
This is the non-witness run with the same scale factors.
image.png
Notice inputs, total puts and total archive sizes have all straightened out. Txs and outputs remain exactly the same and time is very close.
This is the new run running total time over each factor running total.
image.png
And this is the non-witness run with same scale factors.
image.png
evoskuil21:12
The most significant thing to notice overall is that all cost trendlines now continue to have a negative slope from inception to present, whereas without the witness data that trend was reversing. The reason for the reversal was that blocks were getting smaller without the witness data. The graphs clearly show that the more data that is packed into each block, the less average time per byte is spent on total processing. This implies that operations are constant time and that block overhead is less per byte as blocks are bigger.
This of course includes primarily writes, but reads are also significant in these runs, as the block/tx inserts perform a hash table lookup for existing records in three tables (header, txs, tx). This requires ongoing reads across the entire table as deconfliction lists are walked. The extent of the tables increase from zero to 52GB during the run. But the size of the table doesn't affect the paging cost. So I anticipate that other read operations that are algorithmically constant (all of them) will perform as expected.
(edited)
evoskuil22:32
Interesting also that the same number of blocks, txs, hashes, etc. with 20% more data overall takes only .25 hrs longer, out of 10.75 hrs (2.3%).

Sunday, March 26, 2023

evoskuil10:52
Now that we've got full archive population and confirmation of constant time, I've moved on to populating the validation/confirmation metadata tables. I created command line options in bn to scan through all blocks and all transactions while performing the store write queries necessary to set all validation and confirmation states, and to perform the read queries necessary to verify block confirmability (outputs confirmed and not spent). The validation read/write queries may run concurrently, but confirmation states must be set sequentially. This is the only aspect of node sync that is necessarily sequential. So the most important measures are checking for confirmability and setting confirmed. Set end to end these queries for every block set a minimum sync time for the node.
evoskuil12:21
The two sequential queries are is_confirmable(block) and set_strong(block) . The former verifies confirmability (in the confirmation chaser) and the latter confirms the block and all transactions. These are the only material actions in terms of checking spend state and performing confirmation. The former takes 30 minutes and the latter takes 19 minutes - for all blocks/txs/inputs/outputs in the .5TB store. IOW, short of some optimization within these queries (which may be possible), a lower bound on sync time on this machine is 49 minutes. That is below the bound of what is possible on a 1Gbit network connection, so not limiting for practical purposes.
All other actions are parallelizable, so are not independently limiting. The two main write queries for block/tx validation (as opposed to confirmation) are set_confirmable(block) and set_connected(tx). The former completes for all blocks in 2 seconds, and the latter for all 817.5 million txs in 9 minutes - and these are sequential tests. So these are in no way restrictive queries.
These are the two main queries. Note that is_confirmable is quite a complex query, spanning multiple tables.
evoskuil21:08
Had an error in my statement/calcs above for is_confirmable which should be 45 mines (not 30). So along with set_strong the total minimum sequential cost is 64 minutes. Still slightly below the minimum theoretical bandwidth cost at 1Gb/s.
evoskuil21:25
Validation is partially ordered. Only the prevout tx for an input is required to validate the input. Once the set of inputs of a tx is validated, the tx is validated, and once the txs of a block are validated, the block is validated. While eventually all txs must be validated, making sequential validation (block order) the simplest approach, obtaining all txs in block order creates a significant ordering constraint on downloads. In that model the full sync can be no faster than the fastest peer's block transfer rate. But given multiple peers the theoretical limit to the transfer rate (setting aside marshalling cost) is the transfer time of just one block of max size. Given a concurrent peer limit, such as the max number of ports on a machine (2^16), this is currently the transfer time of 12 blocks of average size. So maximizing bandwidth is possible.
The limiting factor is moving data around to achieve the partial validation ordering required by inputs.
evoskuil23:17
Forgot to mention that the orange lines in the graphs above are the individual per-10k block time costs and the blue lines are cumulative. As you can see, constant time.
evoskuil23:24
At design time I was worried about is_confirmable due to the complexity of the relational query. Starting with a block height, the block fk is obtained, then all tx fks, then for each tx, all input fks, then for each input, a search for all spenders of the prevout of that input (by tx-hash/index hash table search key), then each spender is navigated to its tx, then its block fk, then the strong_bk table is searched for block confirmation. There are 2.16 billion inputs. So completing this query in 45 minutes across the entire blockchain is pretty amazing. And I'm glad I didn't try to optimize it with additional tables, since it doesn't appear limiting.
(edited)
evoskuil23:29
This one query determines if the block is spending only confirmed, unspent, mature outputs. We call this confirmation, which is entirely divorced from validation. Validation does not care about anything but input-output script validity and other odd structural constraints.
Confirmation is totally ordered by block whereas validation is partially ordered by input (must have immediate prevout).

Monday, March 27, 2023

evoskuil06:43
Spent the day cleaning up sizing spreadsheets, updating table initialization defaults, reviewing capacity and perf. When some numbers didn't line up exactly I determined that I was incorrectly treating the validation tables as records but I had changed them to slabs so that values could be compressed. This creates a different link sizing requirement, and sure enough when I reviewed the link capacity, the validated_tx table was well over capacity. The validated_bk table was well below. This explained why set_connected(tx) population run accelerated significantly after a certain value. The link had overflowed, resulting in logical failures (which are fast). I'm summing up logical results, but there are expected failures due to the simulation. So after updating the build, the set connected run took 28 mins vs 9. All still constant time. This is still well within criteria, especially given that these calls will run concurrently.
(edited)
evoskuil06:53
Looking at overall sizing, now that I have actual data vs. estimates. We have populated headers, bodies, and validation metadata. The only thing that isn't being populated at this point is query server optional data (address, neutrino) and two potential optimizing tables (bootstrap, buffer). So it's a good look at actual size for the current height. Total headers are 15.3GB at a collision rate of 1.5. This is what gets copied in a snapshot, and takes about 30 secs on my SSD. That can be cut in half by just doubling collision rate. I'll run some tests on idea collision rates at some point. Bodies are 518.6 GB, with some variability possible due to validation behavior. This can be trivially pruned to 501GB by deleting tx validation metadata once synced to a given height. 518 is still below the level projected by my original spreadsheet, at this height (525GB).
(edited)
evoskuil07:15
Tomorrow I'm going to add in chain_state computation into the headers protocol, and look at the performance cost of maintaining that state and adding it into header writes. There should really not be a material cost, as the chain state class never hits the store - it just rolls forward the small amount of history necessary to obtain chain state at and given block. Dummy values are already being written to the store, so there's no change to write cost. I'll follow this up with a full sync that builds on the store with prepopulated headers (and correct chain state). This will provide a store that is then fully capable of self-contained full chain validation.

Tuesday, March 28, 2023

evoskuil11:51
Completed a refactor of chain_state to simplify a bit, including integration with chain::header validation. Moved its initial population from the store into the database query engine (from where it was in the blockchain repo). Wrote some tests and integrated it into node::protocol_header_in. Now have full contextual header validation. Still under 30 secs to obtain, validate, store, and index as candidate each header in the strong header chain.
(edited)

Wednesday, March 29, 2023

evoskuil08:34
Nothing too exciting today. Spent half the day chasing a bug. Was trivial thing, in tested database code, but the test was a little too simple. This was after doing multiple runs against a store to verify idempotency. So that's all good now. New code work was on incorporation of chain_state. This class is designed to init off the store and then prevent the need to ever hit the store (excepting chain splits) to determine chain state. The state just rolls forward in a simple struct from block to block. This was previous implementation that I adapted, almost the same. This hides all fork activation code and various ugly things like that, exposing it as a simple set of flags. Also maintains median time past, height and previous block hash. This can be set up to populate in full from the store for each block (how Core does it) or to roll state forward. I did the former yesterday and added the latter today. I measured the performance difference, and the roll forward cuts the typical full header sync time down from 30 secs to 15 secs. Without any validation or chain state the best I saw was 7-8 secs. With full header validation and rolling chain_state I saw a lot of 11-12 secs. This is obviously inconsequential for initial block download, but it does have bearing on the cost of validating a new block when the header arrives (mining scenario). The total time for one header, including network is about .015 ms.
evoskuil08:44
The aspect that this leaves out presently is the "chaser" object that manages and validates data fed from multiple concurrent peers into a single tree, determining the greatest work, and organizing that into the store. Presently it's just a single peer connection that validates and organizes into the store. The block download is also functional, but as was the case with headers, is not yet validating. Now that headers are storing their context (from chain state), blocks can be validated upon arrival. A couple days ago I did the work to size and verify the validation and confirmation metadata tables. So next step is to validate the full chain as it arrives from a single peer. I'll work on that next. Once that is done I'll implement transaction_in in a similar manner. Its chaser manages the tx pool. Then I'll add in the header/block/tx out protocols, which are even simpler than the incoming protocols. Then will move on to the chasers (header tree, concurrent block downloader/validator/confirmer, tx graph).
(edited)
Still having an exceedingly good experience with the node. Very fast start/stop, predictable resource usage, and never a deadlock or unexplained fault/behavior.

Thursday, March 30, 2023

evoskuil08:57
Got check/accept/connect validation integrated into protocol_block_in . Fixed a simple issue in prevout population. Will be cleaning this up a bit tomorrow and then will do some mainnet verification and and timing runs. Will need to do a small amount of work in witness parameterization and hash caching. At this point we are dangerously close to node v3 (which is blocks-first).
pmienk19:00
Topic: Dev Weekly Status Time: March 30, 2023 01:00pm Pacific Time (US and Canada) Join Zoom Meeting https://us05web.zoom.us/j/87666370902?pwd=bEZDUlJ0MGJ5V3dVMUhVdHl5QzdFdz09 Meeting ID: 876 6637 0902 Passcode: 85U0bF

Friday, March 31, 2023

evoskuil05:14
Ok, got full validation running with my single peer blocks first sequential sync. So no concurrency whatsoever (and no checkpoints). Time flattened out pretty quickly with respect to tx count and total archive byte count. And everything else we've seen has maintained at least constant time for the full chain. Using a linear projection based on the tx rate and byte rate the total time to block 780,000 will be 3.25 days. This includes adding in the known 1.5hs for sequential metadata writes. CPU flat at 2%.
(edited)
✅ 1
evoskuil05:34
The same full sync sans validation completes in 11-12 hours. So validation is 6x that. This is the most naïve possible validation, as a baseline. It's just the serialized block with all prevouts read and attached to inputs, and calls to block.check, .accept and .connect.