Archive of the libbitcoin Slack #general channel.
tx.connect(...) [script validation] at block 307,574. This resulted from optimizations I made in system's elliptic curve wrapper functions. An empty data_slice buffer.data() returns a null pointer (same for std::array and std::vector). This was getting passed into libsecp256k1, resulting in an assert/abort on the null pointer. This is now guarded, as were the other calls. Yesterday I tracked down a regression in the script interpreter, where op_size() was popping vs. peeking. This resulted from my full rewrite of the stack as a variant object processor, vs. serializing all values to/from byte arrays. Neither of these were caught by the full set of bitcoind script tests, which is a strong argument for the value of actual unit testing. The script machine (program, interpreter, stack, and number classes) is now well factored for simple unit testing, but that's not done.wtxid and txid. I had to tweak the offsets txid initially (for a tx count value I missed), and this required me to tweak the offsets on the wtxid (for a similar issue). These hashes really only affect the witness commitment, so that was failing. Second, there was a simple inversion in the witness commitment extraction from the coinbase. I ended up patching this twice.
Another issue was a script number bug. This only came into play in the case of segwit ("clean stack" requirement). The bool value was inverted.
Also, segwit adds the constraint that the null dummy stack value in op_checkmultisig must be "false" (empty vector). The bool value was inverted.
These last two were a result of the variant stack rewrite. The transaction hash optimization is new optimization code. The bip9 thing was just disabled to simplify porting and I forgot to reenable.
I also ran into a disk full error, which was handled cleanly as expected by the database/node, but confused me due to the error text. This killed some time.
Presently it's off and running again. I think that it's through enough segwit blocks that there isn't likely to be much more. Currently at 485422 and completing the run sequentially will take a few days.is_locked() validation, which was a result of non-populating prevout metadata. That prompted me to work off some code debt, organizing the validation functions a little more. This is because validation is split between database-optimized functions for confirmation, and check/accept/connect functions on the chain objects. Previously, and traditionally, confirmability is mixed in with validation. But our parallelism model performs these independently, because they are different concurrency constraints. So now we have check/accept/connect/confirm functions on the chain::header/block/transaction/input classes, where confirm is new. And we don't use the confirm function (which contains is_locked, since it depends on the confirmation height/mtp of the previous output) and instead node performs these checks in the database. To the extent there is any logic in the confirm functions, that is exposed via static methods on the chain classes.tx.set_strong() , which precedes confirmation evaluation. The store is fully concurrent/thread safe read/write. So I just dropped in this one change (adding bc::par_unseq), and got the graphed result for blocks 50k-190k:
TEMPLATE
bool CLASS::set_strong(const header_link& link) NOEXCEPT
{
const auto txs = to_txs(link);
if (txs.empty())
return false;
const table::strong_tx::record strong{ {}, link };
// ========================================================================
const auto scope = store_.get_transactor();
return std::all_of(bc::par_unseq, txs.begin(), txs.end(),
[&](const tx_link& fk) NOEXCEPT
{
return store_.strong_tx.put(fk, strong);
});
// ========================================================================
}
opt line shows percentage of cost after parallelism, starting at no benefit with entirely empty blocks, and then getting down to about 20% of sequential cost, then working its way back up. I haven't done a pair of full runs to see total effect, but trend looks clear.
locked is zero, which is a result of bip68 not being active at these heights. Otherwise this would be up with the other lines. Orange (all/opt) is the total cost, with parallelism. As this is cumulative of the others (except opt), there are obviously side effects from each operation that benefit the others. This is most certainly the result of paging being the dominant cost, with part of the paging cost being shared when the queries run in proximity. Otherwise it's worth noting that no one query really dominates. set strong is a write operation and spent is optimized (necessarily) by a dedicated hash table.locked_prevout and mature_prevout queries are essentially redundant, so can be folded as it pertains to store interaction. That's very good to see, but the numbers I have been looking at are all from heights prior to that query activating. The redundancy arose from my late transfer of relative locktime (bip68) from validation to confirmation.TEMPLATE
bool CLASS::is_match() const NOEXCEPT
{
using namespace system;
BC_ASSERT(!is_add_overflow(link_to_position(link_), Link::size));
if (!memory_)
return false;
auto link = memory_->offset(link_to_position(link_) + Link::size);
if (is_null(link))
return false;
BC_PUSH_WARNING(NO_UNSAFE_COPY_N)
return std::equal(key_.begin(), key_.end(), link);
BC_POP_WARNING()
}
...this is the memory_->offset(...)
TEMPLATE
inline uint8_t* CLASS::offset(size_t bytes) NOEXCEPT
{
if (system::is_greater(bytes, size()))
return nullptr;
return std::next(begin_, bytes);
}
... in other words, the time is being spent inside std::equal(...). (edited)key_ is a std::array, sized to the search key width for a given table. So that's stack allocated and valid. link is a uint8_t* to the memory map. So the hot path tells me that half the time is spent comparing a search key to the memory mapped buffer. This is done in the hashmap conflict list iterator.block: 10000 in 0 secs.[622] block: 20000 in 0 secs.[1152] block: 30000 in 1 secs.[2863] block: 40000 in 1 secs.[5071] block: 50000 in 2 secs.[9443] block: 60000 in 3 secs.[23051] block: 70000 in 4 secs.[45320] block: 80000 in 5 secs.[75748] block: 90000 in 8 secs.[106010] block: 100000 in 13 secs.[192285] block: 110000 in 18 secs.[287135] block: 120000 in 28 secs.[494607] block: 130000 in 48 secs.[900702] block: 140000 in 90 secs.[1752203] block: 150000 in 131 secs.[2573178] block: 160000 in 158 secs.[3264903] block: 170000 in 188 secs.[4277819] block: 180000 in 232 secs.[5754310](edited)
to_spenders(key) - which is a simple input hash table search.
txs encounters 2 conflicts until it gets through half the blocks, then drops to 1. That's the modulo overflow and a target of 1.5 conflicts avg - so perfect distribution. strong_tx is the same, but the table is populated as the run progresses, so no modulo overflow yet.So I'm splitting out the spend index into a 30GB record table that indexes output point to spending tx.
We never hit inputs or outputs now during confirmation.
sequence, point_fk, and parent_fk from the input table. I'm going to run some number to see what sizes would be if these were moved into the spend table and the input table was just the script and witness. This would require an extra link per input, to be stored in the spend table. Apart from the extra space, the downside is that it requires traversal of the spend table (direct, no search) in order to deserialize (read) the input/tx. Writing the data is really not a consequential difference, as both records need to be written and this just shifts from one to the other. Adding the additional link is immaterial at that point. So I'll measure the difference in total read against the difference in confirmation cost.

is_confirmable and set_strong queries combined as pointer vs. iostreams (incremental cost relative to data size). At about 50% completion it's holding steady at about 44%. Not quite the 38% I hoped for, but this should complete at 2.5 hours. Also there is really no perceptible disk activity despite reads across the full archive of indexes.