Skip to content

Adding more property tests and fixing edge case of dropped operation#936

Merged
rphmeier merged 8 commits into
thrumdev:masterfrom
citizen-stig:nikolai/more-tests
May 14, 2026
Merged

Adding more property tests and fixing edge case of dropped operation#936
rphmeier merged 8 commits into
thrumdev:masterfrom
citizen-stig:nikolai/more-tests

Conversation

@citizen-stig
Copy link
Copy Markdown
Contributor

@citizen-stig citizen-stig commented May 12, 2026

As follow up of #931

Includes a fix for:

RangeUpdater::new computed range_end with binary_search_by_key(...).unwrap_or_else(|i| i).

When a key in read_write exactly equals the worker's key_range_end (the max key path of the shard's region), binary_search_by_key returns Ok(i) and unwrap_or_else returns i instead of the required exclusive end i+1.

The matching key is dropped from the worker's range, so the seek is never pushed, no RootPagePending::SubTrie is emitted, and the second commit's root just inherits the first commit's leaf-root unchanged.

Tracing the failing case showed range_end=0 for the second commit (read_write contains only KEY_B = [0xFF; 32], and key_range_end = [0xFF; 32] for the single shard's universe region).

Note from @rphmeier

i will note that this situation would be extremely unlikely to occur with genuinely hashed keys due to the way we shard the key-space among workers, as it essentially requires a hash collision. but good future proofing.

Notable changes

the old account_path filled only bytes 0..16 (for i in 0..4 { path[i*4..][..4]... }); the new one in nomt-test-utils/src/lib.rs:599-611 uses chunks_exact_mut(4) and fills all 32 bytes. This is the real reason add_remove_1000's expected_roots changed — not a "fixture refresh" but a different key distribution. The new behavior is arguably better (more entropy), but it silently changes any test that depended on the old layout

@citizen-stig citizen-stig changed the title Adding more property tests [no merge] Adding more property tests May 12, 2026
Comment thread nomt/tests/add_remove.rs
hex!("516d6911c3b0a36c9227922ca0273a4aee44886201bd186f7ee7e538a769eaa5"),
hex!("381b24719ff91b13d36cf0dd7622f391f4a461452ed7547a46a992ee4a4025aa"),
hex!("207793e2ce76c1feb68c7259f883229f985706c8cc2fcf99f481b622a54ba375"),
hex!("18a457ed03d9d28f8bf84aa05dc3c8005e35564e91d05bf6131094d4f3398528"),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this smells like a breaking change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah...
Byt as far I understand, this is for the test fixture, in reality this will unlikely produce different root hash.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you explain why the root hashes changed here and would not in production?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've mentioned it in PR description. This PR changed how KeyPath is derived from id

Before it has been filling only 16 bytes:

    for i in 0..4 {
        // max(i) == 3, path[12..][..4]
        path[i * 4..][..4].copy_from_slice(&rng.next_u32().to_le_bytes());
    }

pub fn account_path(id: u64) -> KeyPath {
// KeyPaths must be uniformly distributed, but we don't want to spend time on a good hash. So
// the next best option is to use a PRNG seeded with the id.
use rand::{Rng as _, SeedableRng as _};
let mut seed = [0; 16];
seed[0..8].copy_from_slice(&id.to_le_bytes());
let mut rng = rand_pcg::Lcg64Xsh32::from_seed(seed);
let mut path = KeyPath::default();
for i in 0..4 {
path[i * 4..][..4].copy_from_slice(&rng.next_u32().to_le_bytes());
}
path

And now it fills all 32 bytes:

fn seeded_key(seed: u64) -> KeyPath {
let mut rng_seed = [0; 16];
rng_seed[..8].copy_from_slice(&seed.to_le_bytes());
let mut rng = Lcg64Xsh32::from_seed(rng_seed);
let mut key = [0; 32];
for chunk in key.chunks_exact_mut(4) {
chunk.copy_from_slice(&rng.next_u32().to_le_bytes());
}
key

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If we want to play safe, we can do following:

  1. Merge: Remove unused dependencies and align benchtop #939
  2. Do a patch release 1.0.3 -> 1.0.4
  3. Merge this PR
  4. Do a breaking change release, 1.0.4 -> 1.1.0 or even more 2.0 to be more aligned with semver

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No need for that, I think this is OK. i see now that it's just because the test environment changing.

Comment thread nomt/src/merkle/worker.rs
.read_write
.binary_search_by_key(&key_range_end, |x| x.0)
.unwrap_or_else(|i| i);
.partition_point(|(key, _)| *key <= key_range_end);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i see, if key_range_end is actually in the set of ops we would drop it from the range silently. seems like a good fix.

i will note that this situation would be extremely unlikely to occur with genuinely hashed keys due to the way we shard the key-space among workers, as it essentially requires a hash collision. but good future proofing.

citizen-stig and others added 6 commits May 13, 2026 09:14
# Conflicts:
#	nomt/tests/common/mod.rs
Adds the previously-untracked crate the workspace already references —
shared quickcheck Arbitrary impls for KeyPath, DivergingPair,
SharedPrefixCluster, UniqueSortedKeyPaths and helpers used by the
property tests across core and nomt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
property_post_commit_state_matches_reference reads every key in the
expected state, asserts deletes are absent, and compares the Nomt root
to nomt_core::update::build_trie computed independently over the same
final state — gives an oracle decoupled from Nomt's witness/proof path.

property_root_invariant_under_reopen closes the Test instance and
re-opens with cleanup_dir=false, asserting the global root is
unchanged. Catches drift introduced by sync/flush vs in-memory state.

apply_accesses helper deduplicates the read/write/read-then-write
dispatch shared by the new tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@citizen-stig citizen-stig changed the title [no merge] Adding more property tests Adding more property tests and fixing edge case of dropped operation May 13, 2026
@citizen-stig citizen-stig marked this pull request as ready for review May 13, 2026 07:42
@rphmeier rphmeier merged commit 5c8c1f4 into thrumdev:master May 14, 2026
7 checks passed
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