From f950b5e8529df258589ea254569eeb17ebb28f8f Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 7 Oct 2026 10:34:48 +0200 Subject: [PATCH 1/3] fix(drive)!: remove the join window's emptied end date when a contest opens its vote window (PV14) A contest resolved without locking moves its end date from the end of the join window to the full poll duration when a second contender joins. The move removed the entry with an up-tree delete stopping at height 2, the length of `Votes / e`, so GroveDB stopped before the time tree's key and left the time tree behind, empty. The ended-poll read counts an empty time tree toward its limit of 2, so two of them older than every real end date stop all contests from resolving. The delete now stops at height 1, as the document-expiration queue does for the same tree shape: the time tree goes with its last entry, `Votes / e` never does. The move test reads the queue raw, since the end-date query cannot show an empty time tree. Co-Authored-By: Claude Opus 5.5 --- book/src/data-model/contested-documents.md | 3 ++- .../no_locking_contest_tests.rs | 18 ++++++++++++++++-- .../v1/mod.rs | 13 ++++++++----- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/book/src/data-model/contested-documents.md b/book/src/data-model/contested-documents.md index 12e0a18cbcb..a1879d3eaf8 100644 --- a/book/src/data-model/contested-documents.md +++ b/book/src/data-model/contested-documents.md @@ -74,7 +74,8 @@ choices are a contender or abstain. A Lock vote is refused with `VoteChoiceNotAl A contest without locking ends when its join window closes while it still has a single contender, so that contender is awarded the value without a vote window. Its end-date entry is written at the end of the join window when the contest opens; the first additional contender moves it to the full -poll duration, which opens the vote window. `getVotePollsByEndDate` shows whichever end applies. +poll duration, which opens the vote window, and removes the join window's end date when no other +contest ends then. `getVotePollsByEndDate` shows whichever end applies. The moderation charters contract uses this resolution to elect moderation teams. diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs index 6380355cab6..7441b91cae9 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/masternode_vote/no_locking_contest_tests.rs @@ -45,11 +45,12 @@ use drive::drive::votes::paths::{ }; use drive::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::resolve::ContestedDocumentResourceVotePollResolver; use drive::query::VotePollsByEndDateDriveQuery; -use drive::util::test_helpers::setup_contract; +use drive::util::test_helpers::{setup_contract, vote_poll_end_dates}; use platform_version::version::PlatformVersion; use rand::prelude::StdRng; use rand::{Rng, SeedableRng}; use simple_signer::signer::SimpleSigner; +use std::collections::{BTreeMap, BTreeSet}; use std::sync::Arc; /// The DPNS-shaped fixture whose `parentNameAndLabel` index is resolved without locking. @@ -629,6 +630,19 @@ async fn should_open_the_vote_window_when_a_second_contender_joins() { vec![(start + poll_duration, vote_poll(&contract))], "the second contender moves the end to the full poll duration" ); + // Read raw, as the end dates above cannot show a time with no entry: the join window's end + // date goes with its only entry, since an empty one would take a slot of the per-block read + // of ended vote polls + assert_eq!( + vote_poll_end_dates(&platform.drive, platform_version), + BTreeMap::from([( + start + poll_duration, + BTreeSet::from([vote_poll(&contract) + .unique_id() + .expect("expected a vote poll id")]), + )]), + "no end date is left at the end of the join window" + ); // A third contender finds the end date there already join( @@ -674,7 +688,7 @@ async fn should_open_the_vote_window_when_a_second_contender_joins() { Some(bob.0.id()), "plurality" ); - assert!(end_dates(&platform, platform_version).is_empty()); + assert!(vote_poll_end_dates(&platform.drive, platform_version).is_empty()); } #[tokio::test] diff --git a/packages/rs-drive/src/drive/document/insert_contested/add_contested_document_for_contract_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/insert_contested/add_contested_document_for_contract_operations/v1/mod.rs index 073bfc9cfbd..495cbda1176 100644 --- a/packages/rs-drive/src/drive/document/insert_contested/add_contested_document_for_contract_operations/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/insert_contested/add_contested_document_for_contract_operations/v1/mod.rs @@ -1,9 +1,6 @@ use crate::drive::contract::paths::contract_root_path; use crate::drive::document::ContestWindows; -use crate::drive::votes::paths::{ - vote_contested_resource_end_date_queries_at_time_tree_path_vec, - vote_end_date_queries_tree_path_vec, -}; +use crate::drive::votes::paths::vote_contested_resource_end_date_queries_at_time_tree_path_vec; use crate::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePollWithContractInfo; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -30,6 +27,12 @@ use grovedb::batch::KeyInfoPath; use grovedb::{EstimatedLayerInformation, MaybeTree, TransactionArg, TreeType}; use std::collections::HashMap; +/// Where the removal of a moved end-date entry stops climbing: it removes the entry (a key of +/// `Votes / e /