Skip to main content

drive/fees/
op.rs

1use crate::util::batch::GroveDbOpBatch;
2use grovedb_costs::storage_cost::removal::Identifier;
3use grovedb_costs::storage_cost::removal::StorageRemovedBytes::{
4    BasicStorageRemoval, NoStorageRemoval, SectionedStorageRemoval,
5};
6use std::collections::BTreeMap;
7
8use enum_map::Enum;
9use grovedb::batch::key_info::KeyInfo;
10use grovedb::batch::GroveOp;
11use grovedb::batch::KeyInfoPath;
12use grovedb::element::reference_path::ReferencePathType;
13use grovedb::element::IndexAxis;
14use grovedb::element::MaxReferenceHop;
15use grovedb::{batch::QualifiedGroveDbOp, Element, ElementFlags, TreeType};
16use grovedb_costs::OperationCost;
17
18use crate::drive::document::index_level_tree_types::{
19    zero_contribution_wrapper, ZeroContributionRefusal, ZeroContributionWrapper,
20};
21use crate::error::drive::DriveError;
22use crate::error::fee::FeeError;
23use crate::error::Error;
24use crate::fees::get_overflow_error;
25use crate::fees::op::LowLevelDriveOperation::{
26    CalculatedCostOperation, CalculatedEphemeralCostOperation, EphemeralGroveOperation,
27    FunctionOperation, GroveOperation, PreCalculatedFeeResult, RepaidIdentityDebt,
28};
29use crate::util::batch::grovedb_op_batch::GroveDbOpBatchV0Methods;
30#[cfg(test)]
31use crate::util::grove_operations::pending_grove_operations::count_copied_pending_grove_operations;
32use crate::util::storage_flags::StorageFlags;
33use dpp::block::epoch::Epoch;
34use dpp::fee::default_costs::CachedEpochIndexFeeVersions;
35use dpp::fee::fee_result::refunds::FeeRefunds;
36use dpp::fee::fee_result::{FeeResult, LifetimeStorageFees};
37use dpp::fee::Credits;
38use platform_version::version::fee::FeeVersion;
39
40/// Base ops
41#[derive(Debug, Enum)]
42pub enum BaseOp {
43    /// Stop
44    Stop,
45    /// Add
46    Add,
47    /// Multiply
48    Mul,
49    /// Subtract
50    Sub,
51    /// Divide
52    Div,
53    /// Sdiv
54    Sdiv,
55    /// Modulo
56    Mod,
57    /// Smod
58    Smod,
59    /// Addmod
60    Addmod,
61    /// Mulmod
62    Mulmod,
63    /// Signextend
64    Signextend,
65    /// Less than
66    Lt,
67    /// Greater than
68    Gt,
69    /// Slt
70    Slt,
71    /// Sgt
72    Sgt,
73    /// Equals
74    Eq,
75    /// Is zero
76    Iszero,
77    /// And
78    And,
79    /// Or
80    Or,
81    /// Xor
82    Xor,
83    /// Not
84    Not,
85    /// Byte
86    Byte,
87}
88
89impl BaseOp {
90    /// Match the op and get the cost
91    pub fn cost(&self) -> u64 {
92        match self {
93            BaseOp::Stop => 0,
94            BaseOp::Add => 12,
95            BaseOp::Mul => 20,
96            BaseOp::Sub => 12,
97            BaseOp::Div => 20,
98            BaseOp::Sdiv => 20,
99            BaseOp::Mod => 20,
100            BaseOp::Smod => 20,
101            BaseOp::Addmod => 32,
102            BaseOp::Mulmod => 32,
103            BaseOp::Signextend => 20,
104            BaseOp::Lt => 12,
105            BaseOp::Gt => 12,
106            BaseOp::Slt => 12,
107            BaseOp::Sgt => 12,
108            BaseOp::Eq => 12,
109            BaseOp::Iszero => 12,
110            BaseOp::And => 12,
111            BaseOp::Or => 12,
112            BaseOp::Xor => 12,
113            BaseOp::Not => 12,
114            BaseOp::Byte => 12,
115        }
116    }
117}
118
119/// Supported Hash Functions
120#[derive(Debug, Enum, PartialEq, Eq)]
121pub enum HashFunction {
122    /// Used for crypto addresses
123    Sha256RipeMD160,
124    /// Single Sha256
125    Sha256,
126    /// Double Sha256
127    Sha256_2,
128    /// Single Blake3
129    Blake3,
130}
131
132impl HashFunction {
133    fn block_size(&self) -> u16 {
134        match self {
135            HashFunction::Sha256 => 64,
136            HashFunction::Sha256_2 => 64,
137            HashFunction::Blake3 => 64,
138            HashFunction::Sha256RipeMD160 => 64,
139        }
140    }
141
142    fn rounds(&self) -> u16 {
143        match self {
144            HashFunction::Sha256 => 1,
145            HashFunction::Sha256_2 => 2,
146            HashFunction::Blake3 => 1,
147            HashFunction::Sha256RipeMD160 => 1,
148        }
149    }
150
151    fn block_cost(&self, fee_version: &FeeVersion) -> u64 {
152        match self {
153            HashFunction::Sha256 => fee_version.hashing.sha256_per_block,
154            HashFunction::Sha256_2 => fee_version.hashing.sha256_per_block,
155            HashFunction::Blake3 => fee_version.hashing.blake3_per_block,
156            HashFunction::Sha256RipeMD160 => fee_version.hashing.sha256_per_block,
157        }
158    }
159
160    fn base_cost(&self, fee_version: &FeeVersion) -> u64 {
161        match self {
162            HashFunction::Sha256 => fee_version.hashing.single_sha256_base,
163            // It's normal that the base cost for a sha256 will have a single sha256 base
164            // But it has an extra block
165            HashFunction::Sha256_2 => fee_version.hashing.single_sha256_base,
166            HashFunction::Blake3 => fee_version.hashing.blake3_base,
167            HashFunction::Sha256RipeMD160 => fee_version.hashing.sha256_ripe_md160_base,
168        }
169    }
170}
171
172/// A Hash Function Operation
173#[derive(Debug, PartialEq, Eq)]
174pub struct FunctionOp {
175    /// hash
176    pub(crate) hash: HashFunction,
177    /// rounds
178    pub(crate) rounds: u32,
179}
180
181impl FunctionOp {
182    /// The cost of the function
183    fn cost(&self, fee_version: &FeeVersion) -> Credits {
184        let block_cost = (self.rounds as u64).saturating_mul(self.hash.block_cost(fee_version));
185        self.hash.base_cost(fee_version).saturating_add(block_cost)
186    }
187
188    /// Create a new function operation with the following hash knowing the rounds it will take
189    /// in advance
190    pub fn new_with_round_count(hash: HashFunction, rounds: u32) -> Self {
191        FunctionOp { hash, rounds }
192    }
193
194    /// Create a new function operation with the following hash knowing the number of bytes
195    /// it will hash
196    pub fn new_with_byte_count(hash: HashFunction, byte_count: u16) -> Self {
197        let blocks = byte_count / hash.block_size() + 1;
198        let rounds = blocks + hash.rounds() - 1;
199        FunctionOp {
200            hash,
201            rounds: rounds as u32,
202        }
203    }
204}
205
206/// Drive operation
207// GroveOperation dominates every op vec on the write path; boxing it would
208// trade one inline copy for a per-op heap allocation in consensus-critical
209// batching, so the size disparity against the small cost variants is accepted.
210#[allow(clippy::large_enum_variant)]
211#[derive(Debug, Eq, PartialEq)]
212pub enum LowLevelDriveOperation {
213    /// Grove operation
214    GroveOperation(QualifiedGroveDbOp),
215    /// A grove operation writing bytes that provably live a bounded time,
216    /// applied in its own batch (one per [`EphemeralPricing`]) so its added
217    /// bytes can be priced by that rule instead of at the perpetual storage
218    /// price. The elements it writes carry no storage flags (the retag that
219    /// produces it strips them), so their removal refunds nothing. Produced
220    /// for sub-levels of a `timeRange` index that declares a `ttl`
221    /// ([`EphemeralPricing::TimeRangeTtl`]) and for the writes of a document
222    /// whose type declares a `ttl` ([`EphemeralPricing::DocumentTtl`]); both
223    /// keywords are unreachable before protocol v14, where their grammar
224    /// does not parse.
225    EphemeralGroveOperation(QualifiedGroveDbOp, EphemeralPricing),
226    /// A drive operation
227    FunctionOperation(FunctionOp),
228    /// Calculated cost operation
229    CalculatedCostOperation(OperationCost),
230    /// The applied cost of an ephemeral batch — same pricing rule as the
231    /// [`Self::EphemeralGroveOperation`]s it applied, carrying the cost the
232    /// batch application (or its estimation) actually returned.
233    CalculatedEphemeralCostOperation(OperationCost, EphemeralPricing),
234    /// Pre Calculated Fee Result
235    PreCalculatedFeeResult(FeeResult),
236    /// Credits an identity's incoming balance repaid of its debt (its negative credit balance).
237    /// Not a GroveDB operation and no cost: the debt stood for processing fees the identity
238    /// could not pay, which never reached a fee pool, so whoever applies the batch owes these
239    /// credits to the current epoch's processing fee pool. Leaving them out would take them out
240    /// of every balance the credit sum counts. Produced from protocol version 14 only, by
241    /// `add_to_identity_balance_operations` 1; an apply that meets one it does not route fails
242    /// instead of dropping it.
243    RepaidIdentityDebt(Credits),
244}
245
246/// How the added bytes of an ephemeral batch are priced.
247///
248/// Both rules bill the processing of the batch exactly like any other
249/// batch; they differ from ordinary storage only in what an added byte
250/// costs and in never producing refunds (the elements carry no flags).
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub enum EphemeralPricing {
253    /// Entries under a `timeRange` index that declares a `ttl`: every added
254    /// byte bills to processing at the fee table's
255    /// `ttl_ephemeral_disk_usage_credit_per_byte` (the bytes live at most
256    /// `ttl` plus a bounded drainage lag).
257    TimeRangeTtl,
258    /// The writes of a document whose type declares a `ttl`: every added
259    /// byte costs `credit_per_byte`, resolved from the fee schedule's
260    /// `document_ttl` group for the document's remaining lifetime, as a
261    /// storage fee paid out over the `lifetime_epochs` epochs the document
262    /// has left to live (see `FeeResult::lifetime_storage_fees`).
263    DocumentTtl {
264        /// Credits per added byte
265        credit_per_byte: Credits,
266        /// The epochs the document has left to live, at least 1 and at
267        /// most one era
268        lifetime_epochs: u16,
269    },
270}
271
272impl EphemeralPricing {
273    /// The order ephemeral batches apply in, after the standing batch. A
274    /// document's own writes come first: a `timeRange` sub-level with a
275    /// `ttl` may sit under a value tree the same document creates.
276    fn apply_rank(&self) -> u8 {
277        match self {
278            EphemeralPricing::DocumentTtl { .. } => 0,
279            EphemeralPricing::TimeRangeTtl => 1,
280        }
281    }
282}
283
284/// Shared rejection message for the three `Element` wrappers
285/// (`NonCounted` / `NotSummed` / `NotCountedOrSummed`) when asked to wrap an
286/// indexed tree.
287///
288/// grovedb's wrapper constructors reject indexed inners outright, and for a
289/// structural reason rather than an oversight: the wrapper suppresses the
290/// wrapped subtree's contribution to its parent's aggregate, but an indexed
291/// primary's parent element is exactly where that aggregate — and the
292/// secondary root keys derived from it — is committed. A wrapped indexed tree
293/// would have nowhere to hang its secondaries.
294///
295/// The shape that would reach this — a ranked index whose terminal
296/// property-name tree sits inside a value tree that itself aggregates, i.e. a
297/// compound ranked index `[a, b]` on a doctype that ALSO declares an
298/// aggregating index terminating at `[a]` — is rejected at contract-parse
299/// time (`validate_no_ranked_prefix_overlap` in rs-dpp), so this is the
300/// fail-closed backstop behind that check. Failing closed here is deliberate
301/// — the alternative is silently writing a non-indexed tree and having
302/// ranked queries return nothing.
303const INDEXED_INNER_UNWRAPPABLE: &str =
304    "an indexed tree cannot be wrapped in NonCounted / NotSummed / NotCountedOrSummed: the \
305     wrapper suppresses the subtree's contribution to its parent's aggregate, but an indexed \
306     primary commits its aggregate (and the derived secondary root keys) through that very \
307     parent element. A ranked index's terminal property-name tree therefore cannot live inside \
308     an aggregating value tree — i.e. a ranked compound index [a, b] cannot coexist with a \
309     countable/summable index terminating at [a]; contracts declaring that pair are rejected \
310     at parse time.";
311
312impl LowLevelDriveOperation {
313    /// Returns a list of the costs of the Drive operations.
314    /// Should only be used by Calculate fee
315    pub fn consume_to_fees_v0(
316        drive_operations: Vec<LowLevelDriveOperation>,
317        epoch: &Epoch,
318        epochs_per_era: u16,
319        fee_version: &FeeVersion,
320        previous_fee_versions: Option<&CachedEpochIndexFeeVersions>,
321    ) -> Result<Vec<FeeResult>, Error> {
322        drive_operations
323            .into_iter()
324            .map(|operation| match operation {
325                PreCalculatedFeeResult(f) => Ok(f),
326                FunctionOperation(op) => Ok(FeeResult {
327                    processing_fee: op.cost(fee_version),
328                    ..Default::default()
329                }),
330                CalculatedEphemeralCostOperation(cost, EphemeralPricing::DocumentTtl {
331                    credit_per_byte,
332                    lifetime_epochs,
333                }) => {
334                    // The writes of a document whose type declares a `ttl`:
335                    // each added byte costs the price of the document's
336                    // remaining lifetime, as a storage fee the pools pay
337                    // out over the epochs it has left to live. Processing
338                    // is billed as for any batch. Added in place in this
339                    // shipped generation: only a document type parsed from
340                    // the `ttl` keyword, which no protocol version before
341                    // 14 reads, is tagged `DocumentTtl`, so no earlier
342                    // version reaches this arm.
343                    let storage_fee = (cost.storage_cost.added_bytes as u64)
344                        .checked_mul(credit_per_byte)
345                        .ok_or(Error::Fee(FeeError::Overflow(
346                            "overflow pricing the bytes of a document with a time to live",
347                        )))?;
348                    let processing_fee = cost.ephemeral_cost(fee_version)?;
349                    let lifetime_storage_fees = if storage_fee > 0 {
350                        LifetimeStorageFees::from([(lifetime_epochs, storage_fee)])
351                    } else {
352                        LifetimeStorageFees::new()
353                    };
354                    // The elements of such a document carry no storage flags,
355                    // so removals are basic. A sectioned (refundable) removal
356                    // could only come from an element someone else paid for;
357                    // its bytes leave the system all the same and no refund
358                    // is owed through this batch, whose writes refund nothing.
359                    let removed_bytes_from_system =
360                        cost.storage_cost.removed_bytes.total_removed_bytes();
361                    Ok(FeeResult {
362                        storage_fee,
363                        processing_fee,
364                        fee_refunds: FeeRefunds::default(),
365                        removed_bytes_from_system,
366                        lifetime_storage_fees,
367                    })
368                }
369                CalculatedEphemeralCostOperation(cost, EphemeralPricing::TimeRangeTtl) => {
370                    // TTL'd-subtree bytes: the added bytes bill to
371                    // PROCESSING at the ephemeral rate instead of to
372                    // storage — they provably live at most `ttl` plus a
373                    // bounded drainage lag, so the perpetual-retention
374                    // storage price does not apply. No refunds by
375                    // construction: TTL elements carry no storage flags,
376                    // so their removal can only ever be basic.
377                    let ephemeral_bytes_fee = (cost.storage_cost.added_bytes as u64)
378                        .checked_mul(
379                            fee_version
380                                .storage
381                                .ttl_ephemeral_disk_usage_credit_per_byte,
382                        )
383                        .ok_or(Error::Fee(FeeError::Overflow(
384                            "overflow pricing ephemeral bytes",
385                        )))?;
386                    let processing_fee = cost
387                        .ephemeral_cost(fee_version)?
388                        .checked_add(ephemeral_bytes_fee)
389                        .ok_or(Error::Fee(FeeError::Overflow(
390                            "overflow adding ephemeral bytes fee",
391                        )))?;
392                    let removed_bytes_from_system = match cost.storage_cost.removed_bytes {
393                        NoStorageRemoval => 0,
394                        BasicStorageRemoval(amount) => amount,
395                        SectionedStorageRemoval(_) => {
396                            return Err(Error::Drive(DriveError::CorruptedCodeExecution(
397                                "TTL'd subtrees carry no storage flags, so an ephemeral \
398                                 batch cannot produce sectioned (refundable) removal",
399                            )))
400                        }
401                    };
402                    Ok(FeeResult {
403                        storage_fee: 0,
404                        processing_fee,
405                        fee_refunds: FeeRefunds::default(),
406                        removed_bytes_from_system,
407                        lifetime_storage_fees: Default::default(),
408                    })
409                }
410                _ => {
411                    let cost = operation.operation_cost()?;
412                    // There is no need for a checked multiply here because added bytes are u64 and
413                    // storage disk usage credit per byte should never be high enough to cause an overflow
414                    let storage_fee = cost.storage_cost.added_bytes as u64 * fee_version.storage.storage_disk_usage_credit_per_byte;
415                    let processing_fee = cost.ephemeral_cost(fee_version)?;
416                    let (fee_refunds, removed_bytes_from_system) =
417                        match cost.storage_cost.removed_bytes {
418                            NoStorageRemoval => (FeeRefunds::default(), 0),
419                            BasicStorageRemoval(amount) => {
420                                // this is not always considered an error
421                                (FeeRefunds::default(), amount)
422                            }
423                            SectionedStorageRemoval(mut removal_per_epoch_by_identifier) => {
424
425                                let system_amount = removal_per_epoch_by_identifier
426                                    .remove(&Identifier::default())
427                                    .map_or(0, |a| a.values().sum());
428                                if fee_version.fee_version_number == 1 {
429                                    (
430                                        FeeRefunds::from_storage_removal(
431                                            removal_per_epoch_by_identifier,
432                                            epoch.index,
433                                            epochs_per_era,
434                                            &BTreeMap::default(),
435                                        )?,
436                                        system_amount,
437                                    )
438                                } else {
439                                    let previous_fee_versions = previous_fee_versions.ok_or(Error::Drive(DriveError::CorruptedCodeExecution("expected previous epoch index fee versions to be able to offer refunds")))?;
440                                    (
441                                        FeeRefunds::from_storage_removal(
442                                            removal_per_epoch_by_identifier,
443                                            epoch.index,
444                                            epochs_per_era,
445                                            previous_fee_versions,
446                                        )?,
447                                        system_amount,
448                                    )
449                                }
450                            }
451                        };
452                    Ok(FeeResult {
453                        storage_fee,
454                        processing_fee,
455                        fee_refunds,
456                        removed_bytes_from_system,
457                        lifetime_storage_fees: Default::default(),
458                    })
459                }
460            })
461            .collect()
462    }
463
464    /// Returns the cost of this operation
465    pub fn operation_cost(self) -> Result<OperationCost, Error> {
466        match self {
467            GroveOperation(_) | EphemeralGroveOperation(..) => {
468                Err(Error::Drive(DriveError::CorruptedCodeExecution(
469                    "grove operations must be executed, not directly transformed to costs",
470                )))
471            }
472            CalculatedCostOperation(c) | CalculatedEphemeralCostOperation(c, _) => Ok(c),
473            PreCalculatedFeeResult(_) => Err(Error::Drive(DriveError::CorruptedCodeExecution(
474                "pre calculated fees should not be requested by operation costs",
475            ))),
476            FunctionOperation(_) => Err(Error::Drive(DriveError::CorruptedCodeExecution(
477                "function operations should not be requested by operation costs",
478            ))),
479            RepaidIdentityDebt(_) => Err(Error::Drive(DriveError::CorruptedCodeExecution(
480                "a repaid identity debt must be routed to the processing fee pool, not priced",
481            ))),
482        }
483    }
484
485    /// Removes every [`RepaidIdentityDebt`] from `operations` and returns their total: the
486    /// credits the caller owes the current epoch's processing fee pool for the batch.
487    pub fn take_repaid_identity_debt(
488        operations: &mut Vec<LowLevelDriveOperation>,
489    ) -> Result<Credits, Error> {
490        let mut repaid: Credits = 0;
491        let mut overflowed = false;
492        operations.retain(|operation| match operation {
493            RepaidIdentityDebt(credits) => {
494                match repaid.checked_add(*credits) {
495                    Some(total) => repaid = total,
496                    None => overflowed = true,
497                }
498                false
499            }
500            _ => true,
501        });
502        if overflowed {
503            return Err(get_overflow_error("repaid identity debt overflow"));
504        }
505        Ok(repaid)
506    }
507
508    /// Whether `operations` holds a [`RepaidIdentityDebt`], which an apply must route before
509    /// it applies the rest.
510    pub fn holds_repaid_identity_debt(operations: &[LowLevelDriveOperation]) -> bool {
511        operations
512            .iter()
513            .any(|operation| matches!(operation, RepaidIdentityDebt(_)))
514    }
515
516    /// Filters the groveDB ops from a list of operations and puts them in a `GroveDbOpBatch`.
517    pub fn combine_cost_operations(operations: &[LowLevelDriveOperation]) -> OperationCost {
518        let mut cost = OperationCost::default();
519        operations.iter().for_each(|op| {
520            if let CalculatedCostOperation(operation_cost) = op {
521                cost += operation_cost.clone()
522            }
523        });
524        cost
525    }
526
527    /// Filters the groveDB ops from a list of operations and puts them in a `GroveDbOpBatch`.
528    pub fn grovedb_operations_batch(
529        insert_operations: &[LowLevelDriveOperation],
530    ) -> GroveDbOpBatch {
531        let operations: Vec<QualifiedGroveDbOp> = insert_operations
532            .iter()
533            .filter_map(|op| match op {
534                GroveOperation(grovedb_op) | EphemeralGroveOperation(grovedb_op, _) => {
535                    Some(grovedb_op.clone())
536                }
537                _ => None,
538            })
539            .collect();
540        #[cfg(test)]
541        count_copied_pending_grove_operations(operations.len());
542        GroveDbOpBatch::from_operations(operations)
543    }
544
545    /// Filters the groveDB ops from a list of operations and puts them in a `GroveDbOpBatch`.
546    ///
547    /// Every other operation is dropped, a [`RepaidIdentityDebt`] included: its credits are owed
548    /// to a fee pool, so operations that may hold one (identity credits from protocol version
549    /// 14) must go through [`Self::take_repaid_identity_debt`] or an apply that routes it first.
550    pub fn grovedb_operations_batch_consume(
551        insert_operations: Vec<LowLevelDriveOperation>,
552    ) -> GroveDbOpBatch {
553        let operations = insert_operations
554            .into_iter()
555            .filter_map(|op| match op {
556                GroveOperation(grovedb_op) | EphemeralGroveOperation(grovedb_op, _) => {
557                    Some(grovedb_op)
558                }
559                _ => None,
560            })
561            .collect();
562        GroveDbOpBatch::from_operations(operations)
563    }
564
565    /// Filters the ordinary groveDB ops from a list of operations into a
566    /// `GroveDbOpBatch`, returning everything else — ephemeral (TTL'd
567    /// subtree) grove operations included — as leftovers, so no caller can
568    /// lose them or bill them at the ordinary storage price by accident.
569    /// The apply path splits three ways instead
570    /// (`grovedb_operations_batch_consume_split_ephemeral`).
571    pub fn grovedb_operations_batch_consume_with_leftovers(
572        insert_operations: Vec<LowLevelDriveOperation>,
573    ) -> (GroveDbOpBatch, Vec<LowLevelDriveOperation>) {
574        let mut grove_operations = vec![];
575        let mut other_operations = vec![];
576        for op in insert_operations {
577            match op {
578                GroveOperation(grovedb_op) => grove_operations.push(grovedb_op),
579                other => other_operations.push(other),
580            }
581        }
582        (
583            GroveDbOpBatch::from_operations(grove_operations),
584            other_operations,
585        )
586    }
587
588    /// Splits operations three ways: the ordinary grove batch, one
589    /// ephemeral grove batch per [`EphemeralPricing`] — each applied
590    /// separately so its cost can be consumed under its own rule — and
591    /// every non-grove leftover. The ephemeral batches come in the order
592    /// they must apply in (a document's own writes before `timeRange` TTL
593    /// sub-levels, then first appearance), and each keeps its operations in
594    /// their original order.
595    pub fn grovedb_operations_batch_consume_split_ephemeral(
596        insert_operations: Vec<LowLevelDriveOperation>,
597    ) -> (
598        GroveDbOpBatch,
599        Vec<(EphemeralPricing, GroveDbOpBatch)>,
600        Vec<LowLevelDriveOperation>,
601    ) {
602        let mut grove_operations = vec![];
603        let mut ephemeral_groups: Vec<(EphemeralPricing, Vec<QualifiedGroveDbOp>)> = vec![];
604        let mut other_operations = vec![];
605        for op in insert_operations {
606            match op {
607                GroveOperation(grovedb_op) => grove_operations.push(grovedb_op),
608                EphemeralGroveOperation(grovedb_op, pricing) => {
609                    match ephemeral_groups
610                        .iter_mut()
611                        .find(|(group_pricing, _)| *group_pricing == pricing)
612                    {
613                        Some((_, group)) => group.push(grovedb_op),
614                        None => ephemeral_groups.push((pricing, vec![grovedb_op])),
615                    }
616                }
617                other => other_operations.push(other),
618            }
619        }
620        // Stable: groups of one rank keep their first-appearance order.
621        //
622        // Several `DocumentTtl` groups (one document written at two prices)
623        // would apply in first-appearance order, which is only right when no
624        // later group creates a tree an earlier one writes under. No apply
625        // holds two such documents: a documents batch carries one transition
626        // (`max_transitions_in_documents_batch`), and the only operation
627        // writing several documents at once serves system contracts, which
628        // declare no `ttl`. A feature batching document writes must keep one
629        // document's writes in one group or revisit this order.
630        ephemeral_groups.sort_by_key(|(pricing, _)| pricing.apply_rank());
631        (
632            GroveDbOpBatch::from_operations(grove_operations),
633            ephemeral_groups
634                .into_iter()
635                .map(|(pricing, operations)| (pricing, GroveDbOpBatch::from_operations(operations)))
636                .collect(),
637            other_operations,
638        )
639    }
640
641    /// Re-tag an operation as targeting a TTL'd `timeRange` index subtree
642    /// ([`EphemeralPricing::TimeRangeTtl`]); see [`Self::retag_ephemeral_with`].
643    pub fn retag_ephemeral(self) -> LowLevelDriveOperation {
644        self.retag_ephemeral_with(EphemeralPricing::TimeRangeTtl)
645    }
646
647    /// Re-tag one of the writes of a document whose type declares a `ttl`,
648    /// so its bytes are priced for the document's remaining lifetime. Only
649    /// ordinary grove operations move: an operation already ephemeral (a
650    /// `timeRange` TTL sub-level) keeps its own rule, and costs and fee
651    /// results pass through untouched.
652    pub fn retag_document_ttl(self, pricing: EphemeralPricing) -> LowLevelDriveOperation {
653        match self {
654            GroveOperation(_) => self.retag_ephemeral_with(pricing),
655            other => other,
656        }
657    }
658
659    /// Re-tag an operation as ephemeral under `pricing`, so its bytes are
660    /// consumed by that rule. Grove operations move to that rule's batch,
661    /// their elements stripped of storage flags; already-calculated costs
662    /// keep their numbers under the rule; everything else, operations
663    /// already ephemeral included, passes through untouched.
664    pub fn retag_ephemeral_with(self, pricing: EphemeralPricing) -> LowLevelDriveOperation {
665        match self {
666            GroveOperation(mut grovedb_op) => {
667                // TTL'd (ephemeral) subtrees must hold flagless elements:
668                // their bytes are never refundable, and flags on any element
669                // under them would turn its later removal sectioned
670                // (refundable) — the consume path treats that as corruption.
671                // Stripping here, at the single choke point every ephemeral
672                // op passes through, lets the walkers keep building elements
673                // exactly as they do for standing levels.
674                match &mut grovedb_op.op {
675                    GroveOp::InsertWithKnownToNotAlreadyExist { element }
676                    | GroveOp::InsertIfNotExists { element, .. }
677                    | GroveOp::InsertOrReplace { element }
678                    | GroveOp::InsertOrReplaceDontCheckForBackwardsReferences { element }
679                    | GroveOp::Replace { element }
680                    | GroveOp::ReplaceDontCheckForBackwardsReferences { element }
681                    | GroveOp::Patch { element, .. }
682                    | GroveOp::PatchDontCheckForBackwardsReferences { element, .. } => {
683                        element.set_flags(None)
684                    }
685                    GroveOp::RefreshReference { flags, .. } => *flags = None,
686                    _ => {}
687                }
688                EphemeralGroveOperation(grovedb_op, pricing)
689            }
690            CalculatedCostOperation(cost) => CalculatedEphemeralCostOperation(cost, pricing),
691            other => other,
692        }
693    }
694
695    /// Filters the groveDB ops from a list of operations and collects them in a `Vec<QualifiedGroveDbOp>`.
696    ///
697    /// Every other operation is dropped, a [`RepaidIdentityDebt`] included, so operations that
698    /// may hold one must have it routed first (see [`Self::grovedb_operations_batch_consume`]).
699    pub fn grovedb_operations_consume(
700        insert_operations: Vec<LowLevelDriveOperation>,
701    ) -> Vec<QualifiedGroveDbOp> {
702        insert_operations
703            .into_iter()
704            .filter_map(|op| match op {
705                GroveOperation(grovedb_op) | EphemeralGroveOperation(grovedb_op, _) => {
706                    Some(grovedb_op)
707                }
708                _ => None,
709            })
710            .collect()
711    }
712
713    /// Sets `GroveOperation` for inserting an empty tree at the given path and key
714    pub fn for_known_path_key_empty_tree(
715        path: Vec<Vec<u8>>,
716        key: Vec<u8>,
717        storage_flags: Option<&StorageFlags>,
718    ) -> Self {
719        let tree = match storage_flags {
720            Some(storage_flags) => {
721                Element::empty_tree_with_flags(storage_flags.to_some_element_flags())
722            }
723            None => Element::empty_tree(),
724        };
725
726        LowLevelDriveOperation::insert_for_known_path_key_element(path, key, tree)
727    }
728
729    /// Sets `GroveOperation` for inserting an empty sum tree at the given path and key
730    pub fn for_known_path_key_empty_sum_tree(
731        path: Vec<Vec<u8>>,
732        key: Vec<u8>,
733        storage_flags: Option<&StorageFlags>,
734    ) -> Self {
735        let tree = match storage_flags {
736            Some(storage_flags) => {
737                Element::empty_sum_tree_with_flags(storage_flags.to_some_element_flags())
738            }
739            None => Element::empty_sum_tree(),
740        };
741
742        LowLevelDriveOperation::insert_for_known_path_key_element(path, key, tree)
743    }
744
745    /// Sets `GroveOperation` for inserting an empty sum tree at the given path and key
746    pub fn for_known_path_key_empty_big_sum_tree(
747        path: Vec<Vec<u8>>,
748        key: Vec<u8>,
749        storage_flags: Option<&StorageFlags>,
750    ) -> Self {
751        let tree = match storage_flags {
752            Some(storage_flags) => {
753                Element::new_big_sum_tree_with_flags(None, storage_flags.to_some_element_flags())
754            }
755            None => Element::empty_big_sum_tree(),
756        };
757
758        LowLevelDriveOperation::insert_for_known_path_key_element(path, key, tree)
759    }
760
761    /// Sets `GroveOperation` for inserting an empty count tree at the given path and key
762    pub fn for_known_path_key_empty_count_tree(
763        path: Vec<Vec<u8>>,
764        key: Vec<u8>,
765        storage_flags: Option<&StorageFlags>,
766    ) -> Self {
767        let tree = match storage_flags {
768            Some(storage_flags) => {
769                Element::new_count_tree_with_flags(None, storage_flags.to_some_element_flags())
770            }
771            None => Element::empty_count_tree(),
772        };
773
774        LowLevelDriveOperation::insert_for_known_path_key_element(path, key, tree)
775    }
776
777    /// Sets `GroveOperation` for inserting an empty count tree at the given path and key
778    pub fn for_known_path_key_empty_count_sum_tree(
779        path: Vec<Vec<u8>>,
780        key: Vec<u8>,
781        storage_flags: Option<&StorageFlags>,
782    ) -> Self {
783        let tree = match storage_flags {
784            Some(storage_flags) => {
785                Element::new_count_sum_tree_with_flags(None, storage_flags.to_some_element_flags())
786            }
787            None => Element::new_count_sum_tree(None),
788        };
789
790        LowLevelDriveOperation::insert_for_known_path_key_element(path, key, tree)
791    }
792
793    /// Sets `GroveOperation` for inserting an empty `NormalTree` wrapped in
794    /// `Element::NonCounted` at the given path and key. The wrapper makes
795    /// the inserted subtree contribute 0 to a parent count tree's aggregate
796    /// (per grovedb #654). Used by the index-walker for sibling continuations
797    /// inside a `range_countable` value tree, so e.g. a compound `byColorShape`
798    /// continuation under a `byColor` value tree (which is a `CountTree`)
799    /// doesn't pollute the byColor count.
800    pub fn for_known_path_key_empty_non_counted_normal_tree(
801        path: Vec<Vec<u8>>,
802        key: Vec<u8>,
803        storage_flags: Option<&StorageFlags>,
804    ) -> Self {
805        Self::for_known_path_key_empty_non_counted_tree(
806            path,
807            key,
808            TreeType::NormalTree,
809            storage_flags,
810        )
811        .expect("NormalTree NonCounted wrapping never fails")
812    }
813
814    /// Sets `GroveOperation` for inserting an empty tree of the given
815    /// `tree_type` wrapped in `Element::NonCounted`. The wrapper makes the
816    /// inserted subtree contribute 0 to a parent count tree's aggregate
817    /// count (per grovedb #654), regardless of the inner tree variant.
818    ///
819    /// Used by the index walker for sibling continuations inside a
820    /// `range_countable` value tree (a `CountTree`). Most continuations are
821    /// plain `NormalTree`, but in nested-`range_countable` cases (e.g. an
822    /// index `[color]` is range-countable AND a deeper compound index
823    /// `[color, size]` is also range-countable), the continuation
824    /// property-name tree at `"size"` is itself a `ProvableCountTree` and
825    /// must still contribute 0 to the parent `<c1>` `CountTree`.
826    ///
827    /// Returns an error for tree variants whose `NonCounted` wrapping
828    /// hasn't been validated end-to-end yet (currently anything outside
829    /// `NormalTree` / `CountTree` / `ProvableCountTree`).
830    pub fn for_known_path_key_empty_non_counted_tree(
831        path: Vec<Vec<u8>>,
832        key: Vec<u8>,
833        tree_type: TreeType,
834        storage_flags: Option<&StorageFlags>,
835    ) -> Result<Self, Error> {
836        // Per grovedb PR 670, `Element::new_non_counted` only wraps
837        // count-bearing trees — provable-count parents reject the
838        // wrapper at the merk-layer insert guard, and sum-bearing
839        // trees use dedicated `NotSummed` / `NotCountedOrSummed`
840        // wrappers (see [`Self::for_known_path_key_empty_not_summed_tree`]
841        // / [`Self::for_known_path_key_empty_not_counted_or_summed_tree`]).
842        let element_flags = storage_flags.map(|s| s.to_element_flags());
843        let inner = match tree_type {
844            TreeType::NormalTree => Element::empty_tree_with_flags(element_flags),
845            TreeType::CountTree => Element::empty_count_tree_with_flags(element_flags),
846            TreeType::ProvableCountTree => {
847                Element::empty_provable_count_tree_with_flags(element_flags)
848            }
849            TreeType::ProvableSumIndexedTree
850            | TreeType::ProvableCountIndexedTree
851            | TreeType::ProvableCountProvableSumIndexedTree => {
852                return Err(Error::Drive(DriveError::NotSupported(
853                    INDEXED_INNER_UNWRAPPABLE,
854                )));
855            }
856            _ => {
857                return Err(Error::Drive(DriveError::NotSupported(
858                    "NonCounted-wrapping is only supported for NormalTree, CountTree, and \
859                     ProvableCountTree. For sum-bearing continuations under a sum or \
860                     count+sum parent, use `for_known_path_key_empty_not_summed_tree` or \
861                     `for_known_path_key_empty_not_counted_or_summed_tree` instead.",
862                )));
863            }
864        };
865        // Propagate the grovedb error as a typed Drive error rather
866        // than `.expect`-ing. The match above already restricts `inner`
867        // to NormalTree / CountTree / ProvableCountTree — all of which
868        // `new_non_counted` accepts at the head this PR pins
869        // (`packages/rs-drive/Cargo.toml`'s grovedb rev) — so in
870        // practice this `?` is a no-op. Keeping it as `?` means a
871        // future grovedb bump that tightens `new_non_counted`'s
872        // accepted-variant set lands a typed `Error::GroveDB` at the
873        // call site instead of a runtime panic. The `?` conversion
874        // uses `impl From<grovedb::element::error::ElementError>`
875        // defined in `crate::error::mod.rs`.
876        let tree = Element::new_non_counted(inner)?;
877        Ok(LowLevelDriveOperation::insert_for_known_path_key_element(
878            path, key, tree,
879        ))
880    }
881
882    /// Sets `GroveOperation` for inserting an empty sum-bearing tree
883    /// wrapped in `Element::NotSummed` (grovedb PR 670). The wrapper
884    /// makes the inserted subtree contribute 0 to a parent sum tree's
885    /// running sum while still allowing any count it carries to
886    /// propagate normally. Used by the index walker for continuation
887    /// property-name trees inside a `summable`-but-not-`countable`
888    /// value tree. For continuations under a count+sum parent, use
889    /// [`Self::for_known_path_key_empty_not_counted_or_summed_tree`].
890    pub fn for_known_path_key_empty_not_summed_tree(
891        path: Vec<Vec<u8>>,
892        key: Vec<u8>,
893        tree_type: TreeType,
894        storage_flags: Option<&StorageFlags>,
895    ) -> Result<Self, Error> {
896        let element_flags = storage_flags.map(|s| s.to_element_flags());
897        let inner = match tree_type {
898            TreeType::SumTree => Element::empty_sum_tree_with_flags(element_flags),
899            TreeType::BigSumTree => Element::empty_big_sum_tree_with_flags(element_flags),
900            TreeType::ProvableSumTree => Element::empty_provable_sum_tree_with_flags(element_flags),
901            TreeType::CountSumTree => Element::empty_count_sum_tree_with_flags(element_flags),
902            TreeType::ProvableCountSumTree => {
903                Element::empty_provable_count_sum_tree_with_flags(element_flags)
904            }
905            TreeType::ProvableCountProvableSumTree => {
906                Element::empty_provable_count_provable_sum_tree_with_flags(element_flags)
907            }
908            TreeType::ProvableSumIndexedTree
909            | TreeType::ProvableCountIndexedTree
910            | TreeType::ProvableCountProvableSumIndexedTree => {
911                return Err(Error::Drive(DriveError::NotSupported(
912                    INDEXED_INNER_UNWRAPPABLE,
913                )));
914            }
915            _ => {
916                return Err(Error::Drive(DriveError::NotSupported(
917                    "NotSummed-wrapping is only supported for the six sum-bearing tree \
918                     variants (SumTree, BigSumTree, ProvableSumTree, CountSumTree, \
919                     ProvableCountSumTree, ProvableCountProvableSumTree).",
920                )));
921            }
922        };
923        let tree = Element::new_not_summed(inner).map_err(|_| {
924            Error::Drive(DriveError::NotSupported(
925                "Element::new_not_summed rejected the inner tree (unreachable given the \
926                 match above).",
927            ))
928        })?;
929        Ok(LowLevelDriveOperation::insert_for_known_path_key_element(
930            path, key, tree,
931        ))
932    }
933
934    /// Sets `GroveOperation` for inserting an empty inner tree wrapped
935    /// in the wrapper variant appropriate for an `aggregating_parent_tree_type`.
936    ///
937    /// Dispatcher around the three concrete wrapper helpers
938    /// ([`Self::for_known_path_key_empty_non_counted_tree`] /
939    /// [`Self::for_known_path_key_empty_not_summed_tree`] /
940    /// [`Self::for_known_path_key_empty_not_counted_or_summed_tree`])
941    /// keyed on **the parent's** tree type — the wrapper exists to
942    /// suppress contribution to the parent's aggregate, so the parent's
943    /// kind picks the wrapper:
944    /// - Pure count parents (`CountTree` / `ProvableCountTree`) →
945    ///   `Element::NonCounted`.
946    /// - Pure sum parents (`SumTree` / `BigSumTree` / `ProvableSumTree`)
947    ///   → `Element::NotSummed`.
948    /// - Combined count+sum parents (`CountSumTree` /
949    ///   `ProvableCountSumTree` / `ProvableCountProvableSumTree`) →
950    ///   `Element::NotCountedOrSummed`.
951    /// - Non-aggregating parents (`NormalTree`, etc.) — no wrapping
952    ///   needed; caller should use
953    ///   [`crate::fees::op::LowLevelDriveOperationTreeTypeConverter::empty_tree_operation_for_known_path_key`]
954    ///   directly. This dispatcher rejects them with `NotSupported`
955    ///   so an upstream bug surfaces immediately rather than silently
956    ///   emitting an unwrapped child that pollutes a future parent.
957    ///
958    /// `inner_tree_type` is the tree variant being inserted under the
959    /// parent — typically a property-name continuation tree
960    /// (`NormalTree` / `CountTree` / `ProvableCountTree` / their
961    /// sum-bearing siblings).
962    pub fn wrap_in_non_aggregated_for_parent_tree_type(
963        path: Vec<Vec<u8>>,
964        key: Vec<u8>,
965        aggregating_parent_tree_type: TreeType,
966        inner_tree_type: TreeType,
967        storage_flags: Option<&StorageFlags>,
968    ) -> Result<Self, Error> {
969        match aggregating_parent_tree_type {
970            // Count-only parents — wrap so the inner contributes 0 to
971            // the parent's count. The inner can be plain or itself
972            // count-bearing; the helper validates accepted variants.
973            //
974            // `ProvableCountIndexedTree` is included because an indexed
975            // primary aggregates exactly like the tree it mirrors: the
976            // wrapper choice depends on which axes the parent commits, and
977            // PCIT commits the same single count axis as `ProvableCountTree`.
978            TreeType::CountTree
979            | TreeType::ProvableCountTree
980            | TreeType::ProvableCountIndexedTree => {
981                Self::for_known_path_key_empty_non_counted_tree(
982                    path,
983                    key,
984                    inner_tree_type,
985                    storage_flags,
986                )
987            }
988            // Sum-only parents — wrap so the inner contributes 0 to
989            // the parent's sum. Inner must be sum-bearing (see
990            // `for_known_path_key_empty_not_summed_tree`'s accepted set).
991            TreeType::SumTree
992            | TreeType::BigSumTree
993            | TreeType::ProvableSumTree
994            | TreeType::ProvableSumIndexedTree => Self::for_known_path_key_empty_not_summed_tree(
995                path,
996                key,
997                inner_tree_type,
998                storage_flags,
999            ),
1000            // Combined count+sum parents — wrap so both axes contribute
1001            // 0. Inner must be sum-bearing.
1002            TreeType::CountSumTree
1003            | TreeType::ProvableCountSumTree
1004            | TreeType::ProvableCountProvableSumTree
1005            | TreeType::ProvableCountProvableSumIndexedTree => {
1006                Self::for_known_path_key_empty_not_counted_or_summed_tree(
1007                    path,
1008                    key,
1009                    inner_tree_type,
1010                    storage_flags,
1011                )
1012            }
1013            _ => Err(Error::Drive(DriveError::NotSupported(
1014                "wrap_in_non_aggregated_for_parent_tree_type called with a non-aggregating \
1015                 parent tree type — caller should use the unwrapped \
1016                 `empty_tree_operation_for_known_path_key` path instead.",
1017            ))),
1018        }
1019    }
1020
1021    /// Sets `GroveOperation` for inserting an empty sum-bearing tree
1022    /// wrapped in `Element::NotCountedOrSummed` (grovedb PR 670).
1023    /// Suppresses BOTH count and sum propagation to the parent — used
1024    /// for continuation property-name trees under a count+sum
1025    /// aggregating value tree (CountSumTree / ProvableCountSumTree /
1026    /// ProvableCountProvableSumTree). Same accepted inner-type set as
1027    /// [`Self::for_known_path_key_empty_not_summed_tree`].
1028    pub fn for_known_path_key_empty_not_counted_or_summed_tree(
1029        path: Vec<Vec<u8>>,
1030        key: Vec<u8>,
1031        tree_type: TreeType,
1032        storage_flags: Option<&StorageFlags>,
1033    ) -> Result<Self, Error> {
1034        let element_flags = storage_flags.map(|s| s.to_element_flags());
1035        let inner = match tree_type {
1036            TreeType::SumTree => Element::empty_sum_tree_with_flags(element_flags),
1037            TreeType::BigSumTree => Element::empty_big_sum_tree_with_flags(element_flags),
1038            TreeType::ProvableSumTree => Element::empty_provable_sum_tree_with_flags(element_flags),
1039            TreeType::CountSumTree => Element::empty_count_sum_tree_with_flags(element_flags),
1040            TreeType::ProvableCountSumTree => {
1041                Element::empty_provable_count_sum_tree_with_flags(element_flags)
1042            }
1043            TreeType::ProvableCountProvableSumTree => {
1044                Element::empty_provable_count_provable_sum_tree_with_flags(element_flags)
1045            }
1046            TreeType::ProvableSumIndexedTree
1047            | TreeType::ProvableCountIndexedTree
1048            | TreeType::ProvableCountProvableSumIndexedTree => {
1049                return Err(Error::Drive(DriveError::NotSupported(
1050                    INDEXED_INNER_UNWRAPPABLE,
1051                )));
1052            }
1053            _ => {
1054                return Err(Error::Drive(DriveError::NotSupported(
1055                    "NotCountedOrSummed-wrapping is only supported for the six sum-bearing \
1056                     tree variants — see `for_known_path_key_empty_not_summed_tree`.",
1057                )));
1058            }
1059        };
1060        let tree = Element::new_not_counted_or_summed(inner).map_err(|_| {
1061            Error::Drive(DriveError::NotSupported(
1062                "Element::new_not_counted_or_summed rejected the inner tree (unreachable \
1063                 given the match above).",
1064            ))
1065        })?;
1066        Ok(LowLevelDriveOperation::insert_for_known_path_key_element(
1067            path, key, tree,
1068        ))
1069    }
1070
1071    /// Sets `GroveOperation` for inserting an empty continuation tree under an
1072    /// aggregating parent so it contributes **zero to every axis the parent
1073    /// aggregates** — the v2 index walkers' replacement for
1074    /// [`Self::wrap_in_non_aggregated_for_parent_tree_type`].
1075    ///
1076    /// The v0 dispatcher above covers only the diagonal of the parent×inner
1077    /// matrix (count parent + count-ish inner, sum parent + sum-bearing
1078    /// inner, count+sum parent + sum-bearing inner) and errors on everything
1079    /// else, which made shared-prefix aggregate contracts (e.g. a summable
1080    /// `[a]` next to a plain compound `[a, b]`) reject every document
1081    /// insert. This dispatcher completes the matrix using only combinations
1082    /// grovedb accepts:
1083    /// - `CountTree` parent → `Element::NonCounted(inner)` for any inner
1084    ///   tree variant (a `NonCounted` child contributes 0 to the count; the
1085    ///   parent has no sum axis).
1086    /// - `CountSumTree` parent → sum-bearing inner:
1087    ///   `Element::NotCountedOrSummed(inner)`; non-sum inner:
1088    ///   `Element::NonCounted(inner)` (count suppressed by the wrapper, sum
1089    ///   contribution of a non-sum inner is 0 by definition —
1090    ///   `sum_value_or_default()` returns 0 for it).
1091    /// - `SumTree` / `BigSumTree` / `ProvableSumTree` parent → sum-bearing
1092    ///   inner: `Element::NotSummed(inner)`; non-sum inner: **no wrapper at
1093    ///   all** — a non-sum child already contributes 0 to a sum-only
1094    ///   parent, and grovedb has no `NotSummed(non-sum)` form.
1095    /// - Provable count-bearing parents (`ProvableCountTree` /
1096    ///   `ProvableCountSumTree` / `ProvableCountProvableSumTree`) →
1097    ///   `NotSupported`. These commit their count into every node hash and
1098    ///   reject count-suppressed children at grovedb's insert guards
1099    ///   (`TreeType::accepts_non_counted_children` /
1100    ///   `accepts_not_counted_or_summed_children`), so callers must demote
1101    ///   the parent first — see
1102    ///   `crate::drive::document::index_level_tree_types`.
1103    /// - Non-aggregating parents → `NotSupported`; use
1104    ///   [`crate::fees::op::LowLevelDriveOperationTreeTypeConverter::empty_tree_operation_for_known_path_key`]
1105    ///   directly.
1106    ///
1107    /// Only reachable from the v2 index walkers (platform-version gated);
1108    /// the v0 dispatcher stays byte-identical for the frozen v0/v1 walkers.
1109    pub fn for_known_path_key_empty_tree_contributing_zero_to_parent(
1110        path: Vec<Vec<u8>>,
1111        key: Vec<u8>,
1112        aggregating_parent_tree_type: TreeType,
1113        inner_tree_type: TreeType,
1114        storage_flags: Option<&StorageFlags>,
1115    ) -> Result<Self, Error> {
1116        // The decision is shared with the index walkers and
1117        // `drive::document::layout` (`zero_contribution_wrapper`); the
1118        // errors below keep their wording.
1119        match zero_contribution_wrapper(aggregating_parent_tree_type, inner_tree_type) {
1120            Ok(Some(ZeroContributionWrapper::NonCounted)) => {
1121                Self::for_known_path_key_empty_non_counted_any_tree(
1122                    path,
1123                    key,
1124                    inner_tree_type,
1125                    storage_flags,
1126                )
1127            }
1128            Ok(Some(ZeroContributionWrapper::NotCountedOrSummed)) => {
1129                Self::for_known_path_key_empty_not_counted_or_summed_tree(
1130                    path,
1131                    key,
1132                    inner_tree_type,
1133                    storage_flags,
1134                )
1135            }
1136            Ok(Some(ZeroContributionWrapper::NotSummed)) => {
1137                Self::for_known_path_key_empty_not_summed_tree(
1138                    path,
1139                    key,
1140                    inner_tree_type,
1141                    storage_flags,
1142                )
1143            }
1144            Ok(None) => {
1145                inner_tree_type.empty_tree_operation_for_known_path_key(path, key, storage_flags)
1146            }
1147            // An indexed inner is rejected under every aggregating parent,
1148            // including the sum-only ones whose non-sum fallback above is
1149            // unwrapped: a ranked index's terminal property-name tree must
1150            // not live inside an aggregating value tree at all (see
1151            // `INDEXED_INNER_UNWRAPPABLE`), and letting the unwrapped
1152            // fallback quietly accept one would create the exact shape
1153            // rs-dpp's single-property ranked rule and the ranked query
1154            // picker both refuse to serve.
1155            Err(ZeroContributionRefusal::IndexedInner) => Err(Error::Drive(
1156                DriveError::NotSupported(INDEXED_INNER_UNWRAPPABLE),
1157            )),
1158            // Indexed parents are structurally impossible here: the
1159            // ranked upgrade applies to *property-name* trees, and this
1160            // dispatcher is only ever called with a **value** tree as the
1161            // parent. Rejected explicitly rather than through the
1162            // non-aggregating catch-all so a future change that starts
1163            // hanging continuations under an indexed tree reports the real
1164            // reason (the indexed primary's secondaries are keyed by its
1165            // children's aggregates, which a zero-contributing child would
1166            // silently fall out of).
1167            Err(ZeroContributionRefusal::IndexedParent) => {
1168                Err(Error::Drive(DriveError::NotSupported(
1169                    "indexed trees are property-name trees, never value trees, so they cannot \
1170                     host zero-contributing continuation children — see \
1171                     crate::drive::document::ranked_index_tree_type.",
1172                )))
1173            }
1174            Err(ZeroContributionRefusal::ProvableCountParent) => {
1175                Err(Error::Drive(DriveError::NotSupported(
1176                    "provable count-bearing parents cannot host zero-contributing children — \
1177                 grovedb commits their count into every node hash and rejects NonCounted / \
1178                 NotCountedOrSummed children; the index walker must demote such value trees \
1179                 to CountSumTree before hanging continuations under them (see \
1180                 index_level_tree_types_with_continuation_demotion).",
1181                )))
1182            }
1183            Err(ZeroContributionRefusal::NonAggregatingParent) => {
1184                Err(Error::Drive(DriveError::NotSupported(
1185                    "for_known_path_key_empty_tree_contributing_zero_to_parent called with a \
1186                 non-aggregating parent tree type — caller should use the unwrapped \
1187                 `empty_tree_operation_for_known_path_key` path instead.",
1188                )))
1189            }
1190        }
1191    }
1192
1193    /// Sets `GroveOperation` for inserting an empty tree of any of the nine
1194    /// standard merk tree variants wrapped in `Element::NonCounted`.
1195    /// Extends [`Self::for_known_path_key_empty_non_counted_tree`]'s
1196    /// accepted set (`NormalTree` / `CountTree` / `ProvableCountTree`) with
1197    /// the six sum-bearing variants: `Element::new_non_counted` accepts any
1198    /// non-wrapper inner, and under the only parents the v2 walkers use it
1199    /// for (`CountTree`, `CountSumTree` — both without per-node count
1200    /// commitments) the wrapper suppresses the count contribution while a
1201    /// sum-bearing inner's sum still propagates on the parent's sum axis if
1202    /// it has one — which is exactly the v0-diagonal behavior for
1203    /// count-only parents, and unreachable for `CountSumTree` parents (the
1204    /// zero-contribution dispatcher routes their sum-bearing inners through
1205    /// `NotCountedOrSummed` instead).
1206    ///
1207    /// Kept separate from the frozen v0 helper so pre-v14 consensus
1208    /// behavior stays byte-identical.
1209    pub fn for_known_path_key_empty_non_counted_any_tree(
1210        path: Vec<Vec<u8>>,
1211        key: Vec<u8>,
1212        tree_type: TreeType,
1213        storage_flags: Option<&StorageFlags>,
1214    ) -> Result<Self, Error> {
1215        let element_flags = storage_flags.map(|s| s.to_element_flags());
1216        let inner = match tree_type {
1217            TreeType::NormalTree => Element::empty_tree_with_flags(element_flags),
1218            TreeType::SumTree => Element::empty_sum_tree_with_flags(element_flags),
1219            TreeType::BigSumTree => Element::empty_big_sum_tree_with_flags(element_flags),
1220            TreeType::CountTree => Element::empty_count_tree_with_flags(element_flags),
1221            TreeType::CountSumTree => Element::empty_count_sum_tree_with_flags(element_flags),
1222            TreeType::ProvableCountTree => {
1223                Element::empty_provable_count_tree_with_flags(element_flags)
1224            }
1225            TreeType::ProvableCountSumTree => {
1226                Element::empty_provable_count_sum_tree_with_flags(element_flags)
1227            }
1228            TreeType::ProvableSumTree => Element::empty_provable_sum_tree_with_flags(element_flags),
1229            TreeType::ProvableCountProvableSumTree => {
1230                Element::empty_provable_count_provable_sum_tree_with_flags(element_flags)
1231            }
1232            TreeType::ProvableSumIndexedTree
1233            | TreeType::ProvableCountIndexedTree
1234            | TreeType::ProvableCountProvableSumIndexedTree => {
1235                return Err(Error::Drive(DriveError::NotSupported(
1236                    INDEXED_INNER_UNWRAPPABLE,
1237                )));
1238            }
1239            _ => {
1240                return Err(Error::Drive(DriveError::NotSupported(
1241                    "NonCounted-wrapping is only supported for the nine standard merk tree \
1242                     variants; special trees (commitment / MMR / bulk-append / dense) are \
1243                     never index continuation trees.",
1244                )));
1245            }
1246        };
1247        let tree = Element::new_non_counted(inner)?;
1248        Ok(LowLevelDriveOperation::insert_for_known_path_key_element(
1249            path, key, tree,
1250        ))
1251    }
1252
1253    /// Sets `GroveOperation` for inserting an empty provable count tree at the given path and key
1254    pub fn for_known_path_key_empty_provable_count_tree(
1255        path: Vec<Vec<u8>>,
1256        key: Vec<u8>,
1257        storage_flags: Option<&StorageFlags>,
1258    ) -> Self {
1259        let tree = match storage_flags {
1260            Some(storage_flags) => Element::new_provable_count_tree_with_flags(
1261                None,
1262                storage_flags.to_some_element_flags(),
1263            ),
1264            None => Element::empty_provable_count_tree(),
1265        };
1266
1267        LowLevelDriveOperation::insert_for_known_path_key_element(path, key, tree)
1268    }
1269
1270    /// Sets `GroveOperation` for inserting an empty provable sum tree at
1271    /// the given path and key. The provable variant commits aggregated
1272    /// sub-sums to every internal merk node, enabling O(log n)
1273    /// `AggregateSumOnRange` proofs over range queries on the property
1274    /// whose values feed the tree.
1275    ///
1276    /// Used by the index walker for property-name trees of indexes that
1277    /// declare `rangeSummable: true` (mirrors the count-side
1278    /// [`Self::for_known_path_key_empty_provable_count_tree`]).
1279    pub fn for_known_path_key_empty_provable_sum_tree(
1280        path: Vec<Vec<u8>>,
1281        key: Vec<u8>,
1282        storage_flags: Option<&StorageFlags>,
1283    ) -> Self {
1284        let tree = match storage_flags {
1285            Some(storage_flags) => Element::new_provable_sum_tree_with_flags(
1286                None,
1287                storage_flags.to_some_element_flags(),
1288            ),
1289            None => Element::empty_provable_sum_tree(),
1290        };
1291
1292        LowLevelDriveOperation::insert_for_known_path_key_element(path, key, tree)
1293    }
1294
1295    /// Sets `GroveOperation` for inserting an empty provable
1296    /// count-sum tree at the given path and key. **Pre-PR-670
1297    /// variant**: per-node counts committed to every internal merk
1298    /// node, but the sum is only carried at the root (not per-node).
1299    /// Use this when an index declares `rangeCountable: true` plus
1300    /// non-range `summable: "<prop>"` — count queries get the
1301    /// `AggregateCountOnRange` benefit while sum queries return only
1302    /// the root total.
1303    pub fn for_known_path_key_empty_provable_count_sum_tree(
1304        path: Vec<Vec<u8>>,
1305        key: Vec<u8>,
1306        storage_flags: Option<&StorageFlags>,
1307    ) -> Self {
1308        let tree = match storage_flags {
1309            Some(storage_flags) => Element::new_provable_count_sum_tree_with_flags(
1310                None,
1311                storage_flags.to_some_element_flags(),
1312            ),
1313            None => Element::empty_provable_count_sum_tree(),
1314        };
1315
1316        LowLevelDriveOperation::insert_for_known_path_key_element(path, key, tree)
1317    }
1318
1319    /// Sets `GroveOperation` for inserting an empty
1320    /// **provable-count-provable-sum** tree (PCPS) at the given path
1321    /// and key. The grovedb PR 670 newcomer: **both** per-node counts
1322    /// AND per-node sums committed to every internal merk node, so a
1323    /// single tree can answer both `AggregateCountOnRange`,
1324    /// `AggregateSumOnRange`, AND the new
1325    /// `AggregateCountAndSumOnRange` (combined) range queries.
1326    ///
1327    /// Used by the index walker for property-name trees of indexes
1328    /// that declare BOTH `rangeCountable: true` AND `rangeSummable:
1329    /// true`, and for primary-key trees that declare both at the
1330    /// doctype level. The dispatch table in
1331    /// [`crate::drive::document::primary_key_tree_type`]'s v1 arm
1332    /// picks `TreeType::ProvableCountProvableSumTree` for these
1333    /// cases.
1334    pub fn for_known_path_key_empty_provable_count_provable_sum_tree(
1335        path: Vec<Vec<u8>>,
1336        key: Vec<u8>,
1337        storage_flags: Option<&StorageFlags>,
1338    ) -> Self {
1339        let tree = match storage_flags {
1340            Some(storage_flags) => Element::new_provable_count_provable_sum_tree_with_flags(
1341                None,
1342                storage_flags.to_some_element_flags(),
1343            ),
1344            None => Element::empty_provable_count_provable_sum_tree(),
1345        };
1346
1347        LowLevelDriveOperation::insert_for_known_path_key_element(path, key, tree)
1348    }
1349
1350    /// Sets `GroveOperation` for inserting an empty tree at the given path and key
1351    pub fn for_estimated_path_key_empty_tree(
1352        path: KeyInfoPath,
1353        key: KeyInfo,
1354        storage_flags: Option<&StorageFlags>,
1355    ) -> Self {
1356        let tree = match storage_flags {
1357            Some(storage_flags) => {
1358                Element::empty_tree_with_flags(storage_flags.to_some_element_flags())
1359            }
1360            None => Element::empty_tree(),
1361        };
1362
1363        LowLevelDriveOperation::insert_for_estimated_path_key_element(path, key, tree)
1364    }
1365
1366    /// Sets `GroveOperation` for inserting an empty sum tree at the given path and key
1367    pub fn for_estimated_path_key_empty_sum_tree(
1368        path: KeyInfoPath,
1369        key: KeyInfo,
1370        storage_flags: Option<&StorageFlags>,
1371    ) -> Self {
1372        let tree = match storage_flags {
1373            Some(storage_flags) => {
1374                Element::empty_sum_tree_with_flags(storage_flags.to_some_element_flags())
1375            }
1376            None => Element::empty_sum_tree(),
1377        };
1378
1379        LowLevelDriveOperation::insert_for_estimated_path_key_element(path, key, tree)
1380    }
1381
1382    /// Sets `GroveOperation` for inserting an empty count tree at the given (estimated) path and key
1383    pub fn for_estimated_path_key_empty_count_tree(
1384        path: KeyInfoPath,
1385        key: KeyInfo,
1386        storage_flags: Option<&StorageFlags>,
1387    ) -> Self {
1388        let tree = match storage_flags {
1389            Some(storage_flags) => {
1390                Element::empty_count_tree_with_flags(storage_flags.to_some_element_flags())
1391            }
1392            None => Element::empty_count_tree(),
1393        };
1394
1395        LowLevelDriveOperation::insert_for_estimated_path_key_element(path, key, tree)
1396    }
1397
1398    /// Sets `GroveOperation` for inserting an empty provable count tree at the given (estimated) path and key
1399    pub fn for_estimated_path_key_empty_provable_count_tree(
1400        path: KeyInfoPath,
1401        key: KeyInfo,
1402        storage_flags: Option<&StorageFlags>,
1403    ) -> Self {
1404        let tree = match storage_flags {
1405            Some(storage_flags) => {
1406                Element::empty_provable_count_tree_with_flags(storage_flags.to_some_element_flags())
1407            }
1408            None => Element::empty_provable_count_tree(),
1409        };
1410
1411        LowLevelDriveOperation::insert_for_estimated_path_key_element(path, key, tree)
1412    }
1413
1414    /// Cost-estimation analog of
1415    /// [`Self::for_known_path_key_empty_provable_sum_tree`]. See its doc.
1416    pub fn for_estimated_path_key_empty_provable_sum_tree(
1417        path: KeyInfoPath,
1418        key: KeyInfo,
1419        storage_flags: Option<&StorageFlags>,
1420    ) -> Self {
1421        let tree = match storage_flags {
1422            Some(storage_flags) => {
1423                Element::empty_provable_sum_tree_with_flags(storage_flags.to_some_element_flags())
1424            }
1425            None => Element::empty_provable_sum_tree(),
1426        };
1427
1428        LowLevelDriveOperation::insert_for_estimated_path_key_element(path, key, tree)
1429    }
1430
1431    /// Cost-estimation analog of
1432    /// [`Self::for_known_path_key_empty_count_sum_tree`]. See its doc.
1433    pub fn for_estimated_path_key_empty_count_sum_tree(
1434        path: KeyInfoPath,
1435        key: KeyInfo,
1436        storage_flags: Option<&StorageFlags>,
1437    ) -> Self {
1438        let tree = match storage_flags {
1439            Some(storage_flags) => {
1440                Element::empty_count_sum_tree_with_flags(storage_flags.to_some_element_flags())
1441            }
1442            None => Element::empty_count_sum_tree(),
1443        };
1444
1445        LowLevelDriveOperation::insert_for_estimated_path_key_element(path, key, tree)
1446    }
1447
1448    /// Cost-estimation analog of
1449    /// [`Self::for_known_path_key_empty_provable_count_sum_tree`]. See its
1450    /// doc.
1451    pub fn for_estimated_path_key_empty_provable_count_sum_tree(
1452        path: KeyInfoPath,
1453        key: KeyInfo,
1454        storage_flags: Option<&StorageFlags>,
1455    ) -> Self {
1456        let tree = match storage_flags {
1457            Some(storage_flags) => Element::empty_provable_count_sum_tree_with_flags(
1458                storage_flags.to_some_element_flags(),
1459            ),
1460            None => Element::empty_provable_count_sum_tree(),
1461        };
1462
1463        LowLevelDriveOperation::insert_for_estimated_path_key_element(path, key, tree)
1464    }
1465
1466    /// Cost-estimation analog of
1467    /// [`Self::for_known_path_key_empty_provable_count_provable_sum_tree`].
1468    /// See its doc.
1469    pub fn for_estimated_path_key_empty_provable_count_provable_sum_tree(
1470        path: KeyInfoPath,
1471        key: KeyInfo,
1472        storage_flags: Option<&StorageFlags>,
1473    ) -> Self {
1474        let tree = match storage_flags {
1475            Some(storage_flags) => Element::empty_provable_count_provable_sum_tree_with_flags(
1476                storage_flags.to_some_element_flags(),
1477            ),
1478            None => Element::empty_provable_count_provable_sum_tree(),
1479        };
1480
1481        LowLevelDriveOperation::insert_for_estimated_path_key_element(path, key, tree)
1482    }
1483
1484    /// Sets `GroveOperation` for inserting an empty **provable
1485    /// count-indexed** tree (PCIT, grovedb PR 657) at the given path and
1486    /// key. The primary Merk is a byte-compatible mirror of
1487    /// `ProvableCountTree`, so every existing `AggregateCountOnRange` read
1488    /// keeps working against it; what the indexed variant adds is one
1489    /// ordered secondary Merk keyed by `(count_be ‖ child_key)`, which is
1490    /// what makes "top / bottom K groups by document count" O(log n + k)
1491    /// with a proof.
1492    ///
1493    /// Used at contract registration (and by the index walkers when a deeper
1494    /// level is materialized lazily) for an index that declares
1495    /// `rankedCountable: true` while its range layout is count-only. An index
1496    /// that also declares `rangeSummable` lays out as PCPS underneath and
1497    /// therefore takes the multi-axis
1498    /// [`Self::for_known_path_key_empty_provable_count_provable_sum_indexed_tree`]
1499    /// path instead, even when Count is its only ranking axis.
1500    pub fn for_known_path_key_empty_provable_count_indexed_tree(
1501        path: Vec<Vec<u8>>,
1502        key: Vec<u8>,
1503        storage_flags: Option<&StorageFlags>,
1504    ) -> Self {
1505        let tree = match storage_flags {
1506            Some(storage_flags) => Element::empty_provable_count_indexed_tree_with_flags(
1507                storage_flags.to_some_element_flags(),
1508            ),
1509            None => Element::empty_provable_count_indexed_tree(),
1510        };
1511
1512        LowLevelDriveOperation::insert_for_known_path_key_element(path, key, tree)
1513    }
1514
1515    /// Sum-axis counterpart of
1516    /// [`Self::for_known_path_key_empty_provable_count_indexed_tree`]: an
1517    /// empty **provable sum-indexed** tree (PSIT) whose primary mirrors
1518    /// `ProvableSumTree` and whose single secondary is keyed by
1519    /// `(sum_sortable_be ‖ child_key)`.
1520    pub fn for_known_path_key_empty_provable_sum_indexed_tree(
1521        path: Vec<Vec<u8>>,
1522        key: Vec<u8>,
1523        storage_flags: Option<&StorageFlags>,
1524    ) -> Self {
1525        let tree = match storage_flags {
1526            Some(storage_flags) => Element::empty_provable_sum_indexed_tree_with_flags(
1527                storage_flags.to_some_element_flags(),
1528            ),
1529            None => Element::empty_provable_sum_indexed_tree(),
1530        };
1531
1532        LowLevelDriveOperation::insert_for_known_path_key_element(path, key, tree)
1533    }
1534
1535    /// Sets `GroveOperation` for inserting an empty **provable count +
1536    /// provable sum indexed** tree (PCPSIT, grovedb PR 657) at the given path
1537    /// and key, carrying `ranked_axes` — the canonical
1538    /// `(axis_tag, secondary_root_key)` TLV, sorted ascending by tag with no
1539    /// duplicates and 1..=3 entries. Every secondary starts empty, so each
1540    /// root key is `None`.
1541    ///
1542    /// Unlike the two single-axis helpers this one returns a `Result`: the
1543    /// axes list is caller-supplied and grovedb validates it
1544    /// (`Element::validate_pcpsit_axes`), because an out-of-order, duplicated
1545    /// or empty TLV would still be hashed into the parent via `axes_digest`
1546    /// and produce a tree whose secondaries nothing can address.
1547    pub fn for_known_path_key_empty_provable_count_provable_sum_indexed_tree(
1548        path: Vec<Vec<u8>>,
1549        key: Vec<u8>,
1550        ranked_axes: Vec<(u8, Option<Vec<u8>>)>,
1551        storage_flags: Option<&StorageFlags>,
1552    ) -> Result<Self, Error> {
1553        let tree = match storage_flags {
1554            Some(storage_flags) => {
1555                Element::empty_provable_count_provable_sum_indexed_tree_with_flags(
1556                    ranked_axes,
1557                    storage_flags.to_some_element_flags(),
1558                )?
1559            }
1560            None => Element::empty_provable_count_provable_sum_indexed_tree(ranked_axes)?,
1561        };
1562
1563        Ok(LowLevelDriveOperation::insert_for_known_path_key_element(
1564            path, key, tree,
1565        ))
1566    }
1567
1568    /// Dispatcher over the three indexed-tree constructors, keyed on the
1569    /// resolved `tree_type` and the ranking axes that produced it (see
1570    /// [`crate::drive::document::ranked_index_tree_type`]).
1571    ///
1572    /// `ranked_axes` must be non-empty and canonical; the single-axis
1573    /// variants additionally require that the axis matches the variant, since
1574    /// their element shape hard-codes which aggregate the one secondary is
1575    /// keyed by. Any other combination is an upstream resolution bug and is
1576    /// rejected rather than silently narrowed.
1577    pub fn for_known_path_key_empty_indexed_tree(
1578        path: Vec<Vec<u8>>,
1579        key: Vec<u8>,
1580        tree_type: TreeType,
1581        ranked_axes: &[IndexAxis],
1582        storage_flags: Option<&StorageFlags>,
1583    ) -> Result<Self, Error> {
1584        match (tree_type, ranked_axes) {
1585            (TreeType::ProvableCountIndexedTree, [IndexAxis::Count]) => {
1586                Ok(Self::for_known_path_key_empty_provable_count_indexed_tree(
1587                    path,
1588                    key,
1589                    storage_flags,
1590                ))
1591            }
1592            (TreeType::ProvableSumIndexedTree, [IndexAxis::Sum]) => Ok(
1593                Self::for_known_path_key_empty_provable_sum_indexed_tree(path, key, storage_flags),
1594            ),
1595            (TreeType::ProvableCountProvableSumIndexedTree, axes) if !axes.is_empty() => {
1596                Self::for_known_path_key_empty_provable_count_provable_sum_indexed_tree(
1597                    path,
1598                    key,
1599                    axes.iter().map(|axis| (axis.tag(), None)).collect(),
1600                    storage_flags,
1601                )
1602            }
1603            _ => Err(Error::Drive(DriveError::NotSupported(
1604                "for_known_path_key_empty_indexed_tree called with a tree type / ranked-axis \
1605                 pair that does not describe an indexed tree — the single-axis PCIT / PSIT \
1606                 variants accept exactly their own axis and PCPSIT needs a non-empty axis list.",
1607            ))),
1608        }
1609    }
1610
1611    /// Cost-estimation analog of
1612    /// [`Self::for_known_path_key_empty_provable_count_indexed_tree`].
1613    pub fn for_estimated_path_key_empty_provable_count_indexed_tree(
1614        path: KeyInfoPath,
1615        key: KeyInfo,
1616        storage_flags: Option<&StorageFlags>,
1617    ) -> Self {
1618        let tree = match storage_flags {
1619            Some(storage_flags) => Element::empty_provable_count_indexed_tree_with_flags(
1620                storage_flags.to_some_element_flags(),
1621            ),
1622            None => Element::empty_provable_count_indexed_tree(),
1623        };
1624
1625        LowLevelDriveOperation::insert_for_estimated_path_key_element(path, key, tree)
1626    }
1627
1628    /// Cost-estimation analog of
1629    /// [`Self::for_known_path_key_empty_provable_sum_indexed_tree`].
1630    pub fn for_estimated_path_key_empty_provable_sum_indexed_tree(
1631        path: KeyInfoPath,
1632        key: KeyInfo,
1633        storage_flags: Option<&StorageFlags>,
1634    ) -> Self {
1635        let tree = match storage_flags {
1636            Some(storage_flags) => Element::empty_provable_sum_indexed_tree_with_flags(
1637                storage_flags.to_some_element_flags(),
1638            ),
1639            None => Element::empty_provable_sum_indexed_tree(),
1640        };
1641
1642        LowLevelDriveOperation::insert_for_estimated_path_key_element(path, key, tree)
1643    }
1644
1645    /// Cost-estimation analog of
1646    /// [`Self::for_known_path_key_empty_provable_count_provable_sum_indexed_tree`].
1647    pub fn for_estimated_path_key_empty_provable_count_provable_sum_indexed_tree(
1648        path: KeyInfoPath,
1649        key: KeyInfo,
1650        ranked_axes: Vec<(u8, Option<Vec<u8>>)>,
1651        storage_flags: Option<&StorageFlags>,
1652    ) -> Result<Self, Error> {
1653        let tree = match storage_flags {
1654            Some(storage_flags) => {
1655                Element::empty_provable_count_provable_sum_indexed_tree_with_flags(
1656                    ranked_axes,
1657                    storage_flags.to_some_element_flags(),
1658                )?
1659            }
1660            None => Element::empty_provable_count_provable_sum_indexed_tree(ranked_axes)?,
1661        };
1662
1663        Ok(LowLevelDriveOperation::insert_for_estimated_path_key_element(path, key, tree))
1664    }
1665
1666    /// Sets `GroveOperation` for inserting an element at the given path and key
1667    pub fn insert_for_known_path_key_element(
1668        path: Vec<Vec<u8>>,
1669        key: Vec<u8>,
1670        element: Element,
1671    ) -> Self {
1672        GroveOperation(
1673            QualifiedGroveDbOp::insert_or_replace_op(path, key, element)
1674                .dont_check_for_backwards_references(),
1675        )
1676    }
1677
1678    /// Sets `GroveOperation` for replacement of an element at the given path and key
1679    pub fn replace_for_known_path_key_element(
1680        path: Vec<Vec<u8>>,
1681        key: Vec<u8>,
1682        element: Element,
1683    ) -> Self {
1684        GroveOperation(
1685            QualifiedGroveDbOp::replace_op(path, key, element)
1686                .dont_check_for_backwards_references(),
1687        )
1688    }
1689
1690    /// Sets `GroveOperation` for patching of an element at the given path and key
1691    /// This is different from replacement which does not add or delete bytes
1692    pub fn patch_for_known_path_key_element(
1693        path: Vec<Vec<u8>>,
1694        key: Vec<u8>,
1695        element: Element,
1696        change_in_bytes: i32,
1697    ) -> Self {
1698        GroveOperation(
1699            QualifiedGroveDbOp::patch_op(path, key, element, change_in_bytes)
1700                .dont_check_for_backwards_references(),
1701        )
1702    }
1703
1704    /// Sets `GroveOperation` for inserting an element at an unknown estimated path and key
1705    pub fn insert_for_estimated_path_key_element(
1706        path: KeyInfoPath,
1707        key: KeyInfo,
1708        element: Element,
1709    ) -> Self {
1710        GroveOperation(
1711            QualifiedGroveDbOp::insert_estimated_op(path, key, element)
1712                .dont_check_for_backwards_references(),
1713        )
1714    }
1715
1716    /// Sets `GroveOperation` for replacement of an element at an unknown estimated path and key
1717    pub fn replace_for_estimated_path_key_element(
1718        path: KeyInfoPath,
1719        key: KeyInfo,
1720        element: Element,
1721    ) -> Self {
1722        GroveOperation(
1723            QualifiedGroveDbOp::replace_estimated_op(path, key, element)
1724                .dont_check_for_backwards_references(),
1725        )
1726    }
1727
1728    /// Sets `GroveOperation` for refresh of a reference at the given path and key
1729    pub fn refresh_reference_for_known_path_key_reference_info(
1730        path: Vec<Vec<u8>>,
1731        key: Vec<u8>,
1732        reference_path_type: ReferencePathType,
1733        max_reference_hop: MaxReferenceHop,
1734        flags: Option<ElementFlags>,
1735        trust_refresh_reference: bool,
1736    ) -> Self {
1737        GroveOperation(QualifiedGroveDbOp::refresh_reference_op(
1738            path,
1739            key,
1740            reference_path_type,
1741            max_reference_hop,
1742            flags,
1743            // `non_counted: false` — Drive's index references contribute to
1744            // count aggregates on `ProvableCountTree` / `CountTree` parents
1745            // (and to count × sum aggregates on the dual-axis combined
1746            // trees). The non-counted variant exists in grovedb for
1747            // siblings-of-summable-only-trees that must not bump count
1748            // aggregates; Drive never refreshes those.
1749            false,
1750            trust_refresh_reference,
1751        ))
1752    }
1753
1754    /// Sets `GroveOperation` for refresh of a
1755    /// [`grovedb::Element::ReferenceWithSumItem`] at the given path and
1756    /// key, **overriding** the carried sum with `sum_value`.
1757    ///
1758    /// Used by document-update paths on `summable` indexes: when the
1759    /// summed property's value changes but the index keys do not, the
1760    /// reference body stays the same but its sum contribution must be
1761    /// rewritten so ancestor `SumTree` / `ProvableCountSumTree` /
1762    /// `ProvableCountProvableSumTree` aggregates pick up the delta.
1763    ///
1764    /// Mirrors [`Self::refresh_reference_for_known_path_key_reference_info`]
1765    /// but emits a grovedb `RefreshReference` op in
1766    /// `SumItemReference*` mode instead of `PlainReference*` mode.
1767    pub fn refresh_reference_with_sum_item_for_known_path_key_reference_info(
1768        path: Vec<Vec<u8>>,
1769        key: Vec<u8>,
1770        reference_path_type: ReferencePathType,
1771        max_reference_hop: MaxReferenceHop,
1772        sum_value: i64,
1773        flags: Option<ElementFlags>,
1774        trust_refresh_reference: bool,
1775    ) -> Self {
1776        GroveOperation(QualifiedGroveDbOp::refresh_reference_with_sum_item_op(
1777            path,
1778            key,
1779            reference_path_type,
1780            max_reference_hop,
1781            sum_value,
1782            flags,
1783            // `non_counted: false` — see the count-tree rationale on the
1784            // plain-reference helper above. Same reasoning applies on the
1785            // sum side: index references always contribute to ancestor
1786            // count aggregates.
1787            false,
1788            trust_refresh_reference,
1789        ))
1790    }
1791}
1792
1793/// A trait for getting an empty tree operation based on the tree type
1794pub trait LowLevelDriveOperationTreeTypeConverter {
1795    /// Sets `GroveOperation` for inserting an empty tree at the given path and key
1796    fn empty_tree_operation_for_known_path_key(
1797        &self,
1798        path: Vec<Vec<u8>>,
1799        key: Vec<u8>,
1800        storage_flags: Option<&StorageFlags>,
1801    ) -> Result<LowLevelDriveOperation, Error>;
1802}
1803
1804impl LowLevelDriveOperationTreeTypeConverter for TreeType {
1805    /// Sets `GroveOperation` for inserting an empty tree at the given path and key
1806    fn empty_tree_operation_for_known_path_key(
1807        &self,
1808        path: Vec<Vec<u8>>,
1809        key: Vec<u8>,
1810        storage_flags: Option<&StorageFlags>,
1811    ) -> Result<LowLevelDriveOperation, Error> {
1812        let element_flags = storage_flags.map(|storage_flags| storage_flags.to_element_flags());
1813        let element = match self {
1814            TreeType::NormalTree => Element::empty_tree_with_flags(element_flags),
1815            TreeType::SumTree => Element::empty_sum_tree_with_flags(element_flags),
1816            TreeType::BigSumTree => Element::empty_big_sum_tree_with_flags(element_flags),
1817            TreeType::CountTree => Element::empty_count_tree_with_flags(element_flags),
1818            TreeType::CountSumTree => Element::empty_count_sum_tree_with_flags(element_flags),
1819            TreeType::ProvableCountTree => {
1820                Element::empty_provable_count_tree_with_flags(element_flags)
1821            }
1822            TreeType::ProvableCountSumTree => {
1823                Element::empty_provable_count_sum_tree_with_flags(element_flags)
1824            }
1825            TreeType::ProvableCountProvableSumTree => {
1826                Element::empty_provable_count_provable_sum_tree_with_flags(element_flags)
1827            }
1828            TreeType::ProvableSumTree => Element::empty_provable_sum_tree_with_flags(element_flags),
1829            TreeType::CommitmentTree(chunk_power) => {
1830                Element::empty_commitment_tree_with_flags(*chunk_power, element_flags)?
1831            }
1832            TreeType::MmrTree => Element::empty_mmr_tree_with_flags(element_flags),
1833            TreeType::BulkAppendTree(chunk_power) => {
1834                Element::empty_bulk_append_tree_with_flags(*chunk_power, element_flags)?
1835            }
1836            TreeType::DenseAppendOnlyFixedSizeTree(chunk_power) => {
1837                Element::empty_dense_tree_with_flags(*chunk_power, element_flags)
1838            }
1839            // Single-axis indexed trees (grovedb PR 657) carry no axis list
1840            // on the element — the one secondary Merk is implied by the
1841            // variant — so `TreeType` alone fully describes them and the
1842            // generic converter can build them.
1843            TreeType::ProvableSumIndexedTree => {
1844                Element::empty_provable_sum_indexed_tree_with_flags(element_flags)
1845            }
1846            TreeType::ProvableCountIndexedTree => {
1847                Element::empty_provable_count_indexed_tree_with_flags(element_flags)
1848            }
1849            // The multi-axis variant is the exception: its axes TLV lives
1850            // only on the `Element`, `TreeType` does not carry it, and an
1851            // empty TLV is rejected by grovedb's `validate_pcpsit_axes`. The
1852            // conversion is lossy by construction, not merely unimplemented,
1853            // so callers must route through
1854            // [`LowLevelDriveOperation::for_known_path_key_empty_indexed_tree`]
1855            // (or the dedicated
1856            // `batch_insert_empty_provable_count_provable_sum_indexed_tree`
1857            // helper) which take the axes explicitly. Erroring here keeps a
1858            // caller that forgot from silently emitting an axis-less indexed
1859            // tree whose secondaries nothing maintains.
1860            TreeType::ProvableCountProvableSumIndexedTree => {
1861                return Err(Error::Drive(DriveError::NotSupported(
1862                    "empty_tree_operation_for_known_path_key cannot create a \
1863                     ProvableCountProvableSumIndexedTree — the ranked axis set is not carried \
1864                     by TreeType; use for_known_path_key_empty_indexed_tree (or \
1865                     batch_insert_empty_provable_count_provable_sum_indexed_tree) instead.",
1866                )))
1867            }
1868            // A private document store's entry size lives only on the
1869            // `Element` (it does not affect Merk node layout), so `TreeType`
1870            // cannot describe the element to insert. Drive has no private
1871            // document store surface yet; when it does, creation must go
1872            // through a dedicated helper that takes the entry size.
1873            TreeType::PrivateDocumentStore(_) => {
1874                return Err(Error::Drive(DriveError::NotSupported(
1875                    "empty_tree_operation_for_known_path_key cannot create a \
1876                     PrivateDocumentStore — the entry size is not carried by TreeType",
1877                )))
1878            }
1879        };
1880
1881        Ok(LowLevelDriveOperation::insert_for_known_path_key_element(
1882            path, key, element,
1883        ))
1884    }
1885}
1886
1887/// Drive cost trait
1888pub trait DriveCost {
1889    /// Ephemeral cost
1890    fn ephemeral_cost(&self, fee_version: &FeeVersion) -> Result<u64, Error>;
1891}
1892
1893impl DriveCost for OperationCost {
1894    /// Return the ephemeral cost from the operation
1895    fn ephemeral_cost(&self, fee_version: &FeeVersion) -> Result<Credits, Error> {
1896        let OperationCost {
1897            seek_count,
1898            storage_cost,
1899            storage_loaded_bytes,
1900            hash_node_calls,
1901            sinsemilla_hash_calls,
1902        } = self;
1903        let epoch_cost_for_processing_credit_per_byte =
1904            fee_version.storage.storage_processing_credit_per_byte;
1905        let seek_cost = (*seek_count as u64)
1906            .checked_mul(fee_version.storage.storage_seek_cost)
1907            .ok_or_else(|| get_overflow_error("seek cost overflow"))?;
1908        let storage_added_bytes_ephemeral_cost = (storage_cost.added_bytes as u64)
1909            .checked_mul(epoch_cost_for_processing_credit_per_byte)
1910            .ok_or_else(|| get_overflow_error("storage written bytes cost overflow"))?;
1911        let storage_replaced_bytes_ephemeral_cost = (storage_cost.replaced_bytes as u64)
1912            .checked_mul(epoch_cost_for_processing_credit_per_byte)
1913            .ok_or_else(|| get_overflow_error("storage written bytes cost overflow"))?;
1914        let storage_removed_bytes_ephemeral_cost =
1915            (storage_cost.removed_bytes.total_removed_bytes() as u64)
1916                .checked_mul(epoch_cost_for_processing_credit_per_byte)
1917                .ok_or_else(|| get_overflow_error("storage written bytes cost overflow"))?;
1918        // not accessible
1919        let storage_loaded_bytes_cost = { *storage_loaded_bytes }
1920            .checked_mul(fee_version.storage.storage_load_credit_per_byte)
1921            .ok_or_else(|| get_overflow_error("storage loaded cost overflow"))?;
1922
1923        // There is one block per hash node call
1924        let blake3_total = fee_version.hashing.blake3_base + fee_version.hashing.blake3_per_block;
1925        // this can't overflow
1926        let hash_node_cost = blake3_total * (*hash_node_calls as u64);
1927        let sinsemilla_cost = fee_version.hashing.sinsemilla_base * (*sinsemilla_hash_calls as u64);
1928        seek_cost
1929            .checked_add(storage_added_bytes_ephemeral_cost)
1930            .and_then(|c| c.checked_add(storage_replaced_bytes_ephemeral_cost))
1931            .and_then(|c| c.checked_add(storage_loaded_bytes_cost))
1932            .and_then(|c| c.checked_add(storage_removed_bytes_ephemeral_cost))
1933            .and_then(|c| c.checked_add(hash_node_cost))
1934            .and_then(|c| c.checked_add(sinsemilla_cost))
1935            .ok_or_else(|| get_overflow_error("ephemeral cost addition overflow"))
1936    }
1937}
1938
1939#[cfg(test)]
1940#[allow(clippy::identity_op)]
1941mod tests {
1942    use super::*;
1943    use grovedb_costs::storage_cost::removal::StorageRemovedBytes;
1944    use grovedb_costs::storage_cost::StorageCost;
1945    use platform_version::version::fee::storage::FeeStorageVersion;
1946    use platform_version::version::fee::FeeVersion;
1947
1948    /// Helper to get the canonical fee version used across these tests.
1949    fn fee_version() -> &'static FeeVersion {
1950        FeeVersion::first()
1951    }
1952
1953    // ---------------------------------------------------------------
1954    // 1. BaseOp::cost() — spot-check several opcodes
1955    // ---------------------------------------------------------------
1956
1957    #[test]
1958    fn base_op_stop_costs_zero() {
1959        assert_eq!(BaseOp::Stop.cost(), 0);
1960    }
1961
1962    #[test]
1963    fn base_op_add_costs_12() {
1964        assert_eq!(BaseOp::Add.cost(), 12);
1965    }
1966
1967    #[test]
1968    fn base_op_mul_costs_20() {
1969        assert_eq!(BaseOp::Mul.cost(), 20);
1970    }
1971
1972    #[test]
1973    fn base_op_signextend_costs_20() {
1974        assert_eq!(BaseOp::Signextend.cost(), 20);
1975    }
1976
1977    #[test]
1978    fn base_op_addmod_costs_32() {
1979        assert_eq!(BaseOp::Addmod.cost(), 32);
1980    }
1981
1982    #[test]
1983    fn base_op_mulmod_costs_32() {
1984        assert_eq!(BaseOp::Mulmod.cost(), 32);
1985    }
1986
1987    #[test]
1988    fn base_op_byte_costs_12() {
1989        assert_eq!(BaseOp::Byte.cost(), 12);
1990    }
1991
1992    #[test]
1993    fn base_op_sub_costs_12() {
1994        assert_eq!(BaseOp::Sub.cost(), 12);
1995    }
1996
1997    #[test]
1998    fn base_op_div_costs_20() {
1999        assert_eq!(BaseOp::Div.cost(), 20);
2000    }
2001
2002    #[test]
2003    fn base_op_comparison_ops_all_cost_12() {
2004        for op in [
2005            BaseOp::Lt,
2006            BaseOp::Gt,
2007            BaseOp::Slt,
2008            BaseOp::Sgt,
2009            BaseOp::Eq,
2010            BaseOp::Iszero,
2011        ] {
2012            assert_eq!(op.cost(), 12, "comparison op {:?} should cost 12", op);
2013        }
2014    }
2015
2016    #[test]
2017    fn base_op_bitwise_ops_all_cost_12() {
2018        for op in [BaseOp::And, BaseOp::Or, BaseOp::Xor, BaseOp::Not] {
2019            assert_eq!(op.cost(), 12, "bitwise op {:?} should cost 12", op);
2020        }
2021    }
2022
2023    // ---------------------------------------------------------------
2024    // 2. HashFunction — block_size / rounds / block_cost / base_cost
2025    // ---------------------------------------------------------------
2026
2027    #[test]
2028    fn hash_function_block_size_all_64() {
2029        // All four hash functions currently have a 64-byte block size.
2030        assert_eq!(HashFunction::Sha256.block_size(), 64);
2031        assert_eq!(HashFunction::Sha256_2.block_size(), 64);
2032        assert_eq!(HashFunction::Blake3.block_size(), 64);
2033        assert_eq!(HashFunction::Sha256RipeMD160.block_size(), 64);
2034    }
2035
2036    #[test]
2037    fn hash_function_rounds() {
2038        assert_eq!(HashFunction::Sha256.rounds(), 1);
2039        assert_eq!(HashFunction::Sha256_2.rounds(), 2);
2040        assert_eq!(HashFunction::Blake3.rounds(), 1);
2041        assert_eq!(HashFunction::Sha256RipeMD160.rounds(), 1);
2042    }
2043
2044    #[test]
2045    fn hash_function_block_cost_sha256_variants_use_sha256_per_block() {
2046        let fv = fee_version();
2047        let expected = fv.hashing.sha256_per_block;
2048        assert_eq!(HashFunction::Sha256.block_cost(fv), expected);
2049        assert_eq!(HashFunction::Sha256_2.block_cost(fv), expected);
2050        assert_eq!(HashFunction::Sha256RipeMD160.block_cost(fv), expected);
2051    }
2052
2053    #[test]
2054    fn hash_function_block_cost_blake3_uses_blake3_per_block() {
2055        let fv = fee_version();
2056        assert_eq!(
2057            HashFunction::Blake3.block_cost(fv),
2058            fv.hashing.blake3_per_block
2059        );
2060    }
2061
2062    #[test]
2063    fn hash_function_base_cost_sha256() {
2064        let fv = fee_version();
2065        assert_eq!(
2066            HashFunction::Sha256.base_cost(fv),
2067            fv.hashing.single_sha256_base
2068        );
2069    }
2070
2071    #[test]
2072    fn hash_function_base_cost_sha256_2_uses_single_sha256_base() {
2073        let fv = fee_version();
2074        // Sha256_2 intentionally uses single_sha256_base (extra rounds handle the double hash).
2075        assert_eq!(
2076            HashFunction::Sha256_2.base_cost(fv),
2077            fv.hashing.single_sha256_base
2078        );
2079    }
2080
2081    #[test]
2082    fn hash_function_base_cost_blake3() {
2083        let fv = fee_version();
2084        assert_eq!(HashFunction::Blake3.base_cost(fv), fv.hashing.blake3_base);
2085    }
2086
2087    #[test]
2088    fn hash_function_base_cost_sha256_ripe_md160() {
2089        let fv = fee_version();
2090        assert_eq!(
2091            HashFunction::Sha256RipeMD160.base_cost(fv),
2092            fv.hashing.sha256_ripe_md160_base
2093        );
2094    }
2095
2096    // ---------------------------------------------------------------
2097    // 3. FunctionOp::new_with_byte_count — verify blocks/rounds calc
2098    // ---------------------------------------------------------------
2099
2100    #[test]
2101    fn function_op_new_with_byte_count_small_sha256() {
2102        // 32 bytes => blocks = 32/64 + 1 = 1, rounds = 1 + 1 - 1 = 1
2103        let op = FunctionOp::new_with_byte_count(HashFunction::Sha256, 32);
2104        assert_eq!(op.rounds, 1);
2105        assert_eq!(op.hash, HashFunction::Sha256);
2106    }
2107
2108    #[test]
2109    fn function_op_new_with_byte_count_exact_block_boundary_sha256() {
2110        // 64 bytes => blocks = 64/64 + 1 = 2, rounds = 2 + 1 - 1 = 2
2111        let op = FunctionOp::new_with_byte_count(HashFunction::Sha256, 64);
2112        assert_eq!(op.rounds, 2);
2113    }
2114
2115    #[test]
2116    fn function_op_new_with_byte_count_large_sha256() {
2117        // 200 bytes => blocks = 200/64 + 1 = 3 + 1 = 4, rounds = 4 + 1 - 1 = 4
2118        let op = FunctionOp::new_with_byte_count(HashFunction::Sha256, 200);
2119        assert_eq!(op.rounds, 4);
2120    }
2121
2122    #[test]
2123    fn function_op_new_with_byte_count_sha256_2_has_extra_round() {
2124        // 32 bytes => blocks = 32/64 + 1 = 1, rounds = 1 + 2 - 1 = 2
2125        let op = FunctionOp::new_with_byte_count(HashFunction::Sha256_2, 32);
2126        assert_eq!(op.rounds, 2);
2127    }
2128
2129    #[test]
2130    fn function_op_new_with_byte_count_sha256_2_large() {
2131        // 200 bytes => blocks = 200/64 + 1 = 4, rounds = 4 + 2 - 1 = 5
2132        let op = FunctionOp::new_with_byte_count(HashFunction::Sha256_2, 200);
2133        assert_eq!(op.rounds, 5);
2134    }
2135
2136    #[test]
2137    fn function_op_new_with_byte_count_blake3_small() {
2138        // 10 bytes => blocks = 10/64 + 1 = 1, rounds = 1 + 1 - 1 = 1
2139        let op = FunctionOp::new_with_byte_count(HashFunction::Blake3, 10);
2140        assert_eq!(op.rounds, 1);
2141        assert_eq!(op.hash, HashFunction::Blake3);
2142    }
2143
2144    #[test]
2145    fn function_op_new_with_byte_count_blake3_large() {
2146        // 500 bytes => blocks = 500/64 + 1 = 7 + 1 = 8, rounds = 8 + 1 - 1 = 8
2147        let op = FunctionOp::new_with_byte_count(HashFunction::Blake3, 500);
2148        assert_eq!(op.rounds, 8);
2149    }
2150
2151    #[test]
2152    fn function_op_new_with_byte_count_zero_bytes() {
2153        // 0 bytes => blocks = 0/64 + 1 = 1, rounds = 1 + 1 - 1 = 1
2154        let op = FunctionOp::new_with_byte_count(HashFunction::Sha256, 0);
2155        assert_eq!(op.rounds, 1);
2156    }
2157
2158    #[test]
2159    fn function_op_new_with_byte_count_sha256_ripemd160() {
2160        // 20 bytes => blocks = 20/64 + 1 = 1, rounds = 1 + 1 - 1 = 1
2161        let op = FunctionOp::new_with_byte_count(HashFunction::Sha256RipeMD160, 20);
2162        assert_eq!(op.rounds, 1);
2163        assert_eq!(op.hash, HashFunction::Sha256RipeMD160);
2164    }
2165
2166    // ---------------------------------------------------------------
2167    // 4. FunctionOp::cost — verify rounds * block_cost + base_cost
2168    // ---------------------------------------------------------------
2169
2170    #[test]
2171    fn function_op_cost_sha256_one_round() {
2172        let fv = fee_version();
2173        let op = FunctionOp::new_with_round_count(HashFunction::Sha256, 1);
2174        // cost = base + rounds * block_cost = 100 + 1 * 5000 = 5100
2175        let expected = fv.hashing.single_sha256_base + 1 * fv.hashing.sha256_per_block;
2176        assert_eq!(op.cost(fv), expected);
2177    }
2178
2179    #[test]
2180    fn function_op_cost_sha256_2_two_rounds() {
2181        let fv = fee_version();
2182        let op = FunctionOp::new_with_round_count(HashFunction::Sha256_2, 2);
2183        // cost = base + rounds * block_cost = 100 + 2 * 5000 = 10100
2184        let expected = fv.hashing.single_sha256_base + 2 * fv.hashing.sha256_per_block;
2185        assert_eq!(op.cost(fv), expected);
2186    }
2187
2188    #[test]
2189    fn function_op_cost_blake3_one_round() {
2190        let fv = fee_version();
2191        let op = FunctionOp::new_with_round_count(HashFunction::Blake3, 1);
2192        // cost = blake3_base + 1 * blake3_per_block = 100 + 300 = 400
2193        let expected = fv.hashing.blake3_base + 1 * fv.hashing.blake3_per_block;
2194        assert_eq!(op.cost(fv), expected);
2195    }
2196
2197    #[test]
2198    fn function_op_cost_zero_rounds() {
2199        let fv = fee_version();
2200        let op = FunctionOp::new_with_round_count(HashFunction::Blake3, 0);
2201        // cost = blake3_base + 0 * blake3_per_block = blake3_base
2202        assert_eq!(op.cost(fv), fv.hashing.blake3_base);
2203    }
2204
2205    #[test]
2206    fn function_op_cost_from_byte_count_matches_manual_calc() {
2207        let fv = fee_version();
2208        // 128 bytes of SHA256: blocks = 128/64 + 1 = 3, rounds = 3 + 1 - 1 = 3
2209        let op = FunctionOp::new_with_byte_count(HashFunction::Sha256, 128);
2210        assert_eq!(op.rounds, 3);
2211        let expected = fv.hashing.single_sha256_base + 3 * fv.hashing.sha256_per_block;
2212        assert_eq!(op.cost(fv), expected);
2213    }
2214
2215    #[test]
2216    fn function_op_cost_sha256_ripemd160() {
2217        let fv = fee_version();
2218        let op = FunctionOp::new_with_round_count(HashFunction::Sha256RipeMD160, 1);
2219        let expected = fv.hashing.sha256_ripe_md160_base + 1 * fv.hashing.sha256_per_block;
2220        assert_eq!(op.cost(fv), expected);
2221    }
2222
2223    #[test]
2224    fn function_op_cost_saturating_mul_does_not_panic_on_large_rounds() {
2225        let fv = fee_version();
2226        let op = FunctionOp::new_with_round_count(HashFunction::Sha256, u32::MAX);
2227        // u32::MAX as u64 * sha256_per_block (5000) fits in u64 without overflow,
2228        // so cost = base + rounds * block_cost, computed via saturating ops.
2229        let expected_block_cost = (u32::MAX as u64).saturating_mul(fv.hashing.sha256_per_block);
2230        let expected = fv
2231            .hashing
2232            .single_sha256_base
2233            .saturating_add(expected_block_cost);
2234        assert_eq!(op.cost(fv), expected);
2235    }
2236
2237    #[test]
2238    fn function_op_cost_saturates_to_max_with_extreme_fee_version() {
2239        // Construct a fee version where block_cost is large enough that
2240        // u32::MAX * block_cost overflows u64, triggering saturation.
2241        let mut fv = fee_version().clone();
2242        fv.hashing.sha256_per_block = u64::MAX;
2243        let op = FunctionOp::new_with_round_count(HashFunction::Sha256, 2);
2244        // 2 * u64::MAX saturates to u64::MAX, then base.saturating_add(u64::MAX) = u64::MAX.
2245        assert_eq!(op.cost(&fv), u64::MAX);
2246    }
2247
2248    // ---------------------------------------------------------------
2249    // 5. operation_cost() — test all 4 match arms
2250    // ---------------------------------------------------------------
2251
2252    #[test]
2253    fn operation_cost_calculated_cost_operation_returns_cost() {
2254        let cost = OperationCost {
2255            seek_count: 3,
2256            storage_cost: StorageCost {
2257                added_bytes: 100,
2258                replaced_bytes: 50,
2259                removed_bytes: StorageRemovedBytes::NoStorageRemoval,
2260            },
2261            storage_loaded_bytes: 200,
2262            hash_node_calls: 5,
2263            sinsemilla_hash_calls: 0,
2264        };
2265        let op = CalculatedCostOperation(cost.clone());
2266        let result = op.operation_cost().expect("should return Ok");
2267        assert_eq!(result, cost);
2268    }
2269
2270    #[test]
2271    fn operation_cost_grove_operation_returns_error() {
2272        let grove_op = LowLevelDriveOperation::insert_for_known_path_key_element(
2273            vec![vec![1, 2, 3]],
2274            vec![4, 5, 6],
2275            Element::empty_tree(),
2276        );
2277        let result = grove_op.operation_cost();
2278        assert!(result.is_err());
2279        let err_msg = format!("{:?}", result.unwrap_err());
2280        assert!(
2281            err_msg.contains("grove operations must be executed"),
2282            "unexpected error: {}",
2283            err_msg
2284        );
2285    }
2286
2287    #[test]
2288    fn operation_cost_pre_calculated_fee_result_returns_error() {
2289        let fee = FeeResult {
2290            storage_fee: 100,
2291            processing_fee: 200,
2292            ..Default::default()
2293        };
2294        let op = PreCalculatedFeeResult(fee);
2295        let result = op.operation_cost();
2296        assert!(result.is_err());
2297        let err_msg = format!("{:?}", result.unwrap_err());
2298        assert!(
2299            err_msg.contains("pre calculated fees should not be requested"),
2300            "unexpected error: {}",
2301            err_msg
2302        );
2303    }
2304
2305    #[test]
2306    fn operation_cost_function_operation_returns_error() {
2307        let func_op = FunctionOperation(FunctionOp::new_with_round_count(HashFunction::Blake3, 1));
2308        let result = func_op.operation_cost();
2309        assert!(result.is_err());
2310        let err_msg = format!("{:?}", result.unwrap_err());
2311        assert!(
2312            err_msg.contains("function operations should not be requested"),
2313            "unexpected error: {}",
2314            err_msg
2315        );
2316    }
2317
2318    // ---------------------------------------------------------------
2319    // 6. combine_cost_operations — filter and sum
2320    // ---------------------------------------------------------------
2321
2322    #[test]
2323    fn combine_cost_operations_sums_calculated_costs_only() {
2324        let cost1 = OperationCost {
2325            seek_count: 2,
2326            storage_cost: StorageCost {
2327                added_bytes: 10,
2328                replaced_bytes: 0,
2329                removed_bytes: StorageRemovedBytes::NoStorageRemoval,
2330            },
2331            storage_loaded_bytes: 50,
2332            hash_node_calls: 1,
2333            sinsemilla_hash_calls: 0,
2334        };
2335        let cost2 = OperationCost {
2336            seek_count: 3,
2337            storage_cost: StorageCost {
2338                added_bytes: 20,
2339                replaced_bytes: 5,
2340                removed_bytes: StorageRemovedBytes::NoStorageRemoval,
2341            },
2342            storage_loaded_bytes: 100,
2343            hash_node_calls: 2,
2344            sinsemilla_hash_calls: 1,
2345        };
2346
2347        let operations = vec![
2348            CalculatedCostOperation(cost1.clone()),
2349            // This FunctionOperation should be ignored by combine_cost_operations
2350            FunctionOperation(FunctionOp::new_with_round_count(HashFunction::Sha256, 1)),
2351            CalculatedCostOperation(cost2.clone()),
2352            // PreCalculatedFeeResult should also be ignored
2353            PreCalculatedFeeResult(FeeResult::default()),
2354        ];
2355
2356        let combined = LowLevelDriveOperation::combine_cost_operations(&operations);
2357        assert_eq!(combined.seek_count, 2 + 3);
2358        assert_eq!(combined.storage_cost.added_bytes, 10 + 20);
2359        assert_eq!(combined.storage_cost.replaced_bytes, 0 + 5);
2360        assert_eq!(combined.storage_loaded_bytes, 50 + 100);
2361        assert_eq!(combined.hash_node_calls, 1 + 2);
2362        assert_eq!(combined.sinsemilla_hash_calls, 0 + 1);
2363    }
2364
2365    #[test]
2366    fn combine_cost_operations_empty_list_returns_default() {
2367        let combined = LowLevelDriveOperation::combine_cost_operations(&[]);
2368        assert_eq!(combined, OperationCost::default());
2369    }
2370
2371    #[test]
2372    fn combine_cost_operations_no_calculated_costs_returns_default() {
2373        let operations = vec![
2374            FunctionOperation(FunctionOp::new_with_round_count(HashFunction::Blake3, 2)),
2375            PreCalculatedFeeResult(FeeResult {
2376                processing_fee: 999,
2377                ..Default::default()
2378            }),
2379        ];
2380        let combined = LowLevelDriveOperation::combine_cost_operations(&operations);
2381        assert_eq!(combined, OperationCost::default());
2382    }
2383
2384    // ---------------------------------------------------------------
2385    // 7. grovedb_operations_batch / _consume / _consume_with_leftovers
2386    // ---------------------------------------------------------------
2387
2388    /// Helper: creates a GroveOperation variant (insert_or_replace).
2389    fn make_grove_op(key_byte: u8) -> LowLevelDriveOperation {
2390        LowLevelDriveOperation::insert_for_known_path_key_element(
2391            vec![vec![0]],
2392            vec![key_byte],
2393            Element::new_item(vec![key_byte]),
2394        )
2395    }
2396
2397    fn make_mixed_ops() -> Vec<LowLevelDriveOperation> {
2398        vec![
2399            make_grove_op(1),
2400            FunctionOperation(FunctionOp::new_with_round_count(HashFunction::Sha256, 1)),
2401            make_grove_op(2),
2402            CalculatedCostOperation(OperationCost::default()),
2403            make_grove_op(3),
2404        ]
2405    }
2406
2407    #[test]
2408    fn grovedb_operations_batch_filters_grove_ops_from_ref() {
2409        let ops = make_mixed_ops();
2410        let batch = LowLevelDriveOperation::grovedb_operations_batch(&ops);
2411        assert_eq!(batch.len(), 3);
2412    }
2413
2414    #[test]
2415    fn grovedb_operations_batch_empty_input() {
2416        let batch = LowLevelDriveOperation::grovedb_operations_batch(&[]);
2417        assert!(batch.is_empty());
2418    }
2419
2420    #[test]
2421    fn grovedb_operations_batch_no_grove_ops() {
2422        let ops = vec![
2423            FunctionOperation(FunctionOp::new_with_round_count(HashFunction::Blake3, 1)),
2424            CalculatedCostOperation(OperationCost::default()),
2425        ];
2426        let batch = LowLevelDriveOperation::grovedb_operations_batch(&ops);
2427        assert!(batch.is_empty());
2428    }
2429
2430    #[test]
2431    fn grovedb_operations_batch_consume_filters_grove_ops() {
2432        let ops = make_mixed_ops();
2433        let batch = LowLevelDriveOperation::grovedb_operations_batch_consume(ops);
2434        assert_eq!(batch.len(), 3);
2435    }
2436
2437    #[test]
2438    fn grovedb_operations_batch_consume_empty_input() {
2439        let batch = LowLevelDriveOperation::grovedb_operations_batch_consume(vec![]);
2440        assert!(batch.is_empty());
2441    }
2442
2443    #[test]
2444    fn grovedb_operations_batch_consume_with_leftovers_partitions_correctly() {
2445        let ops = make_mixed_ops();
2446        let (batch, leftovers) =
2447            LowLevelDriveOperation::grovedb_operations_batch_consume_with_leftovers(ops);
2448        assert_eq!(batch.len(), 3);
2449        assert_eq!(leftovers.len(), 2);
2450
2451        // Verify leftovers contain the non-grove operations.
2452        for leftover in &leftovers {
2453            assert!(
2454                !matches!(leftover, GroveOperation(_)),
2455                "leftovers should not contain GroveOperation variants"
2456            );
2457        }
2458    }
2459
2460    #[test]
2461    fn grovedb_operations_batch_consume_with_leftovers_all_grove() {
2462        let ops = vec![make_grove_op(10), make_grove_op(20)];
2463        let (batch, leftovers) =
2464            LowLevelDriveOperation::grovedb_operations_batch_consume_with_leftovers(ops);
2465        assert_eq!(batch.len(), 2);
2466        assert!(leftovers.is_empty());
2467    }
2468
2469    #[test]
2470    fn grovedb_operations_batch_consume_with_leftovers_no_grove() {
2471        let ops = vec![
2472            CalculatedCostOperation(OperationCost::default()),
2473            FunctionOperation(FunctionOp::new_with_round_count(HashFunction::Sha256, 1)),
2474        ];
2475        let (batch, leftovers) =
2476            LowLevelDriveOperation::grovedb_operations_batch_consume_with_leftovers(ops);
2477        assert!(batch.is_empty());
2478        assert_eq!(leftovers.len(), 2);
2479    }
2480
2481    #[test]
2482    fn grovedb_operations_batch_consume_with_leftovers_keeps_ephemeral_ops() {
2483        let ops = vec![
2484            make_grove_op(10),
2485            make_grove_op(20).retag_ephemeral(),
2486            CalculatedCostOperation(OperationCost::default()),
2487        ];
2488        let (batch, leftovers) =
2489            LowLevelDriveOperation::grovedb_operations_batch_consume_with_leftovers(ops);
2490        assert_eq!(batch.len(), 1, "only the ordinary grove op joins the batch");
2491        assert_eq!(leftovers.len(), 2);
2492        assert!(
2493            leftovers
2494                .iter()
2495                .any(|op| matches!(op, EphemeralGroveOperation(..))),
2496            "an ephemeral grove op must survive as a leftover, never be dropped"
2497        );
2498    }
2499
2500    #[test]
2501    fn grovedb_operations_batch_consume_with_leftovers_empty() {
2502        let (batch, leftovers) =
2503            LowLevelDriveOperation::grovedb_operations_batch_consume_with_leftovers(vec![]);
2504        assert!(batch.is_empty());
2505        assert!(leftovers.is_empty());
2506    }
2507
2508    // ---------------------------------------------------------------
2509    // 8. DriveCost::ephemeral_cost — various scenarios
2510    // ---------------------------------------------------------------
2511
2512    #[test]
2513    fn ephemeral_cost_zero_operation() {
2514        let fv = fee_version();
2515        let cost = OperationCost::default();
2516        let result = cost.ephemeral_cost(fv).expect("should not overflow");
2517        assert_eq!(result, 0);
2518    }
2519
2520    #[test]
2521    fn ephemeral_cost_seek_only() {
2522        let fv = fee_version();
2523        let cost = OperationCost {
2524            seek_count: 5,
2525            storage_cost: StorageCost::default(),
2526            storage_loaded_bytes: 0,
2527            hash_node_calls: 0,
2528            sinsemilla_hash_calls: 0,
2529        };
2530        let result = cost.ephemeral_cost(fv).expect("should not overflow");
2531        let expected = 5u64 * fv.storage.storage_seek_cost;
2532        assert_eq!(result, expected);
2533    }
2534
2535    #[test]
2536    fn ephemeral_cost_storage_added_bytes() {
2537        let fv = fee_version();
2538        let cost = OperationCost {
2539            seek_count: 0,
2540            storage_cost: StorageCost {
2541                added_bytes: 100,
2542                replaced_bytes: 0,
2543                removed_bytes: StorageRemovedBytes::NoStorageRemoval,
2544            },
2545            storage_loaded_bytes: 0,
2546            hash_node_calls: 0,
2547            sinsemilla_hash_calls: 0,
2548        };
2549        let result = cost.ephemeral_cost(fv).expect("should not overflow");
2550        let expected = 100u64 * fv.storage.storage_processing_credit_per_byte;
2551        assert_eq!(result, expected);
2552    }
2553
2554    #[test]
2555    fn ephemeral_cost_storage_replaced_bytes() {
2556        let fv = fee_version();
2557        let cost = OperationCost {
2558            seek_count: 0,
2559            storage_cost: StorageCost {
2560                added_bytes: 0,
2561                replaced_bytes: 50,
2562                removed_bytes: StorageRemovedBytes::NoStorageRemoval,
2563            },
2564            storage_loaded_bytes: 0,
2565            hash_node_calls: 0,
2566            sinsemilla_hash_calls: 0,
2567        };
2568        let result = cost.ephemeral_cost(fv).expect("should not overflow");
2569        let expected = 50u64 * fv.storage.storage_processing_credit_per_byte;
2570        assert_eq!(result, expected);
2571    }
2572
2573    #[test]
2574    fn ephemeral_cost_storage_removed_bytes_basic() {
2575        let fv = fee_version();
2576        let cost = OperationCost {
2577            seek_count: 0,
2578            storage_cost: StorageCost {
2579                added_bytes: 0,
2580                replaced_bytes: 0,
2581                removed_bytes: StorageRemovedBytes::BasicStorageRemoval(75),
2582            },
2583            storage_loaded_bytes: 0,
2584            hash_node_calls: 0,
2585            sinsemilla_hash_calls: 0,
2586        };
2587        let result = cost.ephemeral_cost(fv).expect("should not overflow");
2588        let expected = 75u64 * fv.storage.storage_processing_credit_per_byte;
2589        assert_eq!(result, expected);
2590    }
2591
2592    #[test]
2593    fn ephemeral_cost_loaded_bytes() {
2594        let fv = fee_version();
2595        let cost = OperationCost {
2596            seek_count: 0,
2597            storage_cost: StorageCost::default(),
2598            storage_loaded_bytes: 300,
2599            hash_node_calls: 0,
2600            sinsemilla_hash_calls: 0,
2601        };
2602        let result = cost.ephemeral_cost(fv).expect("should not overflow");
2603        let expected = 300u64 * fv.storage.storage_load_credit_per_byte;
2604        assert_eq!(result, expected);
2605    }
2606
2607    #[test]
2608    fn ephemeral_cost_hash_node_calls() {
2609        let fv = fee_version();
2610        let cost = OperationCost {
2611            seek_count: 0,
2612            storage_cost: StorageCost::default(),
2613            storage_loaded_bytes: 0,
2614            hash_node_calls: 10,
2615            sinsemilla_hash_calls: 0,
2616        };
2617        let result = cost.ephemeral_cost(fv).expect("should not overflow");
2618        let blake3_total = fv.hashing.blake3_base + fv.hashing.blake3_per_block;
2619        let expected = blake3_total * 10;
2620        assert_eq!(result, expected);
2621    }
2622
2623    #[test]
2624    fn ephemeral_cost_sinsemilla_hash_calls() {
2625        let fv = fee_version();
2626        let cost = OperationCost {
2627            seek_count: 0,
2628            storage_cost: StorageCost::default(),
2629            storage_loaded_bytes: 0,
2630            hash_node_calls: 0,
2631            sinsemilla_hash_calls: 3,
2632        };
2633        let result = cost.ephemeral_cost(fv).expect("should not overflow");
2634        let expected = fv.hashing.sinsemilla_base * 3;
2635        assert_eq!(result, expected);
2636    }
2637
2638    #[test]
2639    fn ephemeral_cost_all_components_combined() {
2640        let fv = fee_version();
2641        let cost = OperationCost {
2642            seek_count: 2,
2643            storage_cost: StorageCost {
2644                added_bytes: 10,
2645                replaced_bytes: 20,
2646                removed_bytes: StorageRemovedBytes::BasicStorageRemoval(30),
2647            },
2648            storage_loaded_bytes: 40,
2649            hash_node_calls: 5,
2650            sinsemilla_hash_calls: 1,
2651        };
2652        let result = cost.ephemeral_cost(fv).expect("should not overflow");
2653
2654        let seek_cost = 2u64 * fv.storage.storage_seek_cost;
2655        let processing_per_byte = fv.storage.storage_processing_credit_per_byte;
2656        let added_cost = 10u64 * processing_per_byte;
2657        let replaced_cost = 20u64 * processing_per_byte;
2658        let removed_cost = 30u64 * processing_per_byte;
2659        let loaded_cost = 40u64 * fv.storage.storage_load_credit_per_byte;
2660        let blake3_total = fv.hashing.blake3_base + fv.hashing.blake3_per_block;
2661        let hash_cost = blake3_total * 5;
2662        let sinsemilla_cost = fv.hashing.sinsemilla_base * 1;
2663
2664        let expected = seek_cost
2665            + added_cost
2666            + replaced_cost
2667            + loaded_cost
2668            + removed_cost
2669            + hash_cost
2670            + sinsemilla_cost;
2671        assert_eq!(result, expected);
2672    }
2673
2674    #[test]
2675    fn ephemeral_cost_overflow_seek_cost() {
2676        let fv = &FeeVersion {
2677            storage: FeeStorageVersion {
2678                storage_seek_cost: u64::MAX,
2679                ..fee_version().storage.clone()
2680            },
2681            ..fee_version().clone()
2682        };
2683        let cost = OperationCost {
2684            seek_count: 2, // 2 * u64::MAX overflows
2685            storage_cost: StorageCost::default(),
2686            storage_loaded_bytes: 0,
2687            hash_node_calls: 0,
2688            sinsemilla_hash_calls: 0,
2689        };
2690        let result = cost.ephemeral_cost(fv);
2691        assert!(result.is_err(), "expected overflow error for seek cost");
2692    }
2693
2694    #[test]
2695    fn ephemeral_cost_overflow_storage_written_bytes() {
2696        let fv = &FeeVersion {
2697            storage: FeeStorageVersion {
2698                storage_processing_credit_per_byte: u64::MAX,
2699                ..fee_version().storage.clone()
2700            },
2701            ..fee_version().clone()
2702        };
2703        let cost = OperationCost {
2704            seek_count: 0,
2705            storage_cost: StorageCost {
2706                added_bytes: 2, // 2 * u64::MAX overflows
2707                replaced_bytes: 0,
2708                removed_bytes: StorageRemovedBytes::NoStorageRemoval,
2709            },
2710            storage_loaded_bytes: 0,
2711            hash_node_calls: 0,
2712            sinsemilla_hash_calls: 0,
2713        };
2714        let result = cost.ephemeral_cost(fv);
2715        assert!(
2716            result.is_err(),
2717            "expected overflow error for storage written bytes"
2718        );
2719    }
2720
2721    #[test]
2722    fn ephemeral_cost_overflow_loaded_bytes() {
2723        let fv = &FeeVersion {
2724            storage: FeeStorageVersion {
2725                storage_load_credit_per_byte: u64::MAX,
2726                ..fee_version().storage.clone()
2727            },
2728            ..fee_version().clone()
2729        };
2730        let cost = OperationCost {
2731            seek_count: 0,
2732            storage_cost: StorageCost::default(),
2733            storage_loaded_bytes: 2, // 2 * u64::MAX overflows
2734            hash_node_calls: 0,
2735            sinsemilla_hash_calls: 0,
2736        };
2737        let result = cost.ephemeral_cost(fv);
2738        assert!(
2739            result.is_err(),
2740            "expected overflow error for loaded bytes cost"
2741        );
2742    }
2743
2744    /// Covers the `TreeType::ProvableSumTree` arm of
2745    /// `LowLevelDriveOperationTreeTypeConverter::empty_tree_operation_for_known_path_key`
2746    /// added by the grovedb#661 bump. Drive doesn't currently construct
2747    /// `ProvableSumTree` anywhere else, so without this test the new arm is
2748    /// uncovered.
2749    #[test]
2750    fn empty_tree_operation_for_known_path_key_provable_sum_tree() {
2751        use grovedb::batch::GroveOp;
2752
2753        let op = TreeType::ProvableSumTree
2754            .empty_tree_operation_for_known_path_key(vec![b"root".to_vec()], b"k".to_vec(), None)
2755            .expect("empty_tree_operation_for_known_path_key");
2756
2757        match op {
2758            LowLevelDriveOperation::GroveOperation(grove_op) => match grove_op.op {
2759                GroveOp::InsertOrReplace { element }
2760                | GroveOp::InsertOrReplaceDontCheckForBackwardsReferences { element } => assert!(
2761                    matches!(element, Element::ProvableSumTree(..)),
2762                    "expected ProvableSumTree element, got: {:?}",
2763                    element
2764                ),
2765                other => panic!("expected GroveOp::InsertOrReplace, got: {:?}", other),
2766            },
2767            other => panic!("expected GroveOperation, got: {:?}", other),
2768        }
2769    }
2770
2771    /// Table-driven pin of the v14 zero-contribution dispatcher: every
2772    /// accepted parent × inner cell must produce exactly the specified
2773    /// wrapper (or an unwrapped tree), and every rejected parent must
2774    /// error for every inner. This decides consensus-relevant element
2775    /// shapes for v14 continuation inserts, so a regression here (or a
2776    /// demotion-helper change routing a provable parent in) must fail
2777    /// loudly.
2778    #[test]
2779    fn zero_contribution_dispatcher_full_matrix() {
2780        use grovedb::batch::GroveOp;
2781
2782        const ALL_INNERS: [TreeType; 9] = [
2783            TreeType::NormalTree,
2784            TreeType::SumTree,
2785            TreeType::BigSumTree,
2786            TreeType::CountTree,
2787            TreeType::CountSumTree,
2788            TreeType::ProvableCountTree,
2789            TreeType::ProvableCountSumTree,
2790            TreeType::ProvableSumTree,
2791            TreeType::ProvableCountProvableSumTree,
2792        ];
2793
2794        fn is_sum_bearing(tree_type: TreeType) -> bool {
2795            matches!(
2796                tree_type,
2797                TreeType::SumTree
2798                    | TreeType::BigSumTree
2799                    | TreeType::CountSumTree
2800                    | TreeType::ProvableCountSumTree
2801                    | TreeType::ProvableSumTree
2802                    | TreeType::ProvableCountProvableSumTree
2803            )
2804        }
2805
2806        fn element_tree_type(element: &Element) -> TreeType {
2807            match element {
2808                Element::Tree(..) => TreeType::NormalTree,
2809                Element::SumTree(..) => TreeType::SumTree,
2810                Element::BigSumTree(..) => TreeType::BigSumTree,
2811                Element::CountTree(..) => TreeType::CountTree,
2812                Element::CountSumTree(..) => TreeType::CountSumTree,
2813                Element::ProvableCountTree(..) => TreeType::ProvableCountTree,
2814                Element::ProvableCountSumTree(..) => TreeType::ProvableCountSumTree,
2815                Element::ProvableSumTree(..) => TreeType::ProvableSumTree,
2816                Element::ProvableCountProvableSumTree(..) => TreeType::ProvableCountProvableSumTree,
2817                other => panic!("unexpected inner element: {other:?}"),
2818            }
2819        }
2820
2821        #[derive(Debug, PartialEq)]
2822        enum Expected {
2823            NonCounted,
2824            NotSummed,
2825            NotCountedOrSummed,
2826            Unwrapped,
2827        }
2828
2829        let dispatch = |parent: TreeType, inner: TreeType| {
2830            LowLevelDriveOperation::for_known_path_key_empty_tree_contributing_zero_to_parent(
2831                vec![b"root".to_vec()],
2832                b"key".to_vec(),
2833                parent,
2834                inner,
2835                None,
2836            )
2837        };
2838
2839        let assert_cell = |parent: TreeType, inner: TreeType, expected: Expected| {
2840            let op = dispatch(parent, inner).unwrap_or_else(|error| {
2841                panic!("parent {parent:?} inner {inner:?} must be accepted: {error}")
2842            });
2843            let element = match op {
2844                LowLevelDriveOperation::GroveOperation(grove_op) => match grove_op.op {
2845                    GroveOp::InsertOrReplace { element }
2846                    | GroveOp::InsertOrReplaceDontCheckForBackwardsReferences { element } => {
2847                        element
2848                    }
2849                    other => panic!("expected InsertOrReplace, got {other:?}"),
2850                },
2851                other => panic!("expected GroveOperation, got {other:?}"),
2852            };
2853            let (wrapper, produced_inner) = match &element {
2854                Element::NonCounted(inner_element) => {
2855                    (Expected::NonCounted, inner_element.as_ref())
2856                }
2857                Element::NotSummed(inner_element) => (Expected::NotSummed, inner_element.as_ref()),
2858                Element::NotCountedOrSummed(inner_element) => {
2859                    (Expected::NotCountedOrSummed, inner_element.as_ref())
2860                }
2861                plain => (Expected::Unwrapped, plain),
2862            };
2863            assert_eq!(
2864                wrapper, expected,
2865                "parent {parent:?} inner {inner:?}: wrong wrapper"
2866            );
2867            assert_eq!(
2868                element_tree_type(produced_inner),
2869                inner,
2870                "parent {parent:?} inner {inner:?}: wrong inner tree type"
2871            );
2872        };
2873
2874        // Count-only parents wrap every inner NonCounted.
2875        for inner in ALL_INNERS {
2876            assert_cell(TreeType::CountTree, inner, Expected::NonCounted);
2877        }
2878        // Count-sum parents: sum-bearing inners get NotCountedOrSummed,
2879        // non-sum inners get NonCounted.
2880        for inner in ALL_INNERS {
2881            let expected = if is_sum_bearing(inner) {
2882                Expected::NotCountedOrSummed
2883            } else {
2884                Expected::NonCounted
2885            };
2886            assert_cell(TreeType::CountSumTree, inner, expected);
2887        }
2888        // Sum-only parents: sum-bearing inners get NotSummed, non-sum
2889        // inners are inserted unwrapped (they contribute 0 naturally).
2890        for parent in [
2891            TreeType::SumTree,
2892            TreeType::BigSumTree,
2893            TreeType::ProvableSumTree,
2894        ] {
2895            for inner in ALL_INNERS {
2896                let expected = if is_sum_bearing(inner) {
2897                    Expected::NotSummed
2898                } else {
2899                    Expected::Unwrapped
2900                };
2901                assert_cell(parent, inner, expected);
2902            }
2903        }
2904        // Provable count-bearing parents can't host zero-contributing
2905        // children (the walkers demote them first); non-aggregating
2906        // parents should use the plain path. Both must error for every
2907        // inner.
2908        for parent in [
2909            TreeType::NormalTree,
2910            TreeType::ProvableCountTree,
2911            TreeType::ProvableCountSumTree,
2912            TreeType::ProvableCountProvableSumTree,
2913        ] {
2914            for inner in ALL_INNERS {
2915                assert!(
2916                    dispatch(parent, inner).is_err(),
2917                    "parent {parent:?} inner {inner:?} must be rejected"
2918                );
2919            }
2920        }
2921
2922        // Ranked (indexed) trees are property-name trees, never value
2923        // trees, and can never be a continuation inside an aggregating
2924        // value tree — rejected in both roles, for every counterpart,
2925        // including the sum-only parents whose non-sum inners are
2926        // otherwise inserted unwrapped.
2927        const INDEXED: [TreeType; 3] = [
2928            TreeType::ProvableCountIndexedTree,
2929            TreeType::ProvableSumIndexedTree,
2930            TreeType::ProvableCountProvableSumIndexedTree,
2931        ];
2932        for indexed in INDEXED {
2933            for inner in ALL_INNERS {
2934                assert!(
2935                    dispatch(indexed, inner).is_err(),
2936                    "indexed parent {indexed:?} inner {inner:?} must be rejected"
2937                );
2938            }
2939            for parent in [
2940                TreeType::CountTree,
2941                TreeType::CountSumTree,
2942                TreeType::SumTree,
2943                TreeType::BigSumTree,
2944                TreeType::ProvableSumTree,
2945            ] {
2946                assert!(
2947                    dispatch(parent, indexed).is_err(),
2948                    "parent {parent:?} indexed inner {indexed:?} must be rejected"
2949                );
2950            }
2951        }
2952    }
2953
2954    #[test]
2955    fn ephemeral_cost_overflow_in_addition_chain() {
2956        // Use values that individually do not overflow but whose sum does.
2957        let fv = fee_version();
2958        let cost = OperationCost {
2959            seek_count: u32::MAX,
2960            storage_cost: StorageCost {
2961                added_bytes: u32::MAX,
2962                replaced_bytes: u32::MAX,
2963                removed_bytes: StorageRemovedBytes::BasicStorageRemoval(u32::MAX),
2964            },
2965            storage_loaded_bytes: u64::MAX,
2966            hash_node_calls: u32::MAX,
2967            sinsemilla_hash_calls: u32::MAX,
2968        };
2969        let result = cost.ephemeral_cost(fv);
2970        assert!(
2971            result.is_err(),
2972            "expected overflow error when summing large components"
2973        );
2974    }
2975}