diem/diem · error

Transaction with empty write set should be discarded.

Error message

Transaction with empty write set should be discarded.

What it means

process_vm_outputs requires every transaction the Move VM told the executor to Keep (commit) to actually mutate state; a Keep status with an empty write set is treated as an invariant violation and triggers this error. The executor deliberately refuses to commit such transactions because a committed transaction must produce a new state root and a non-empty TransactionInfo.

Source

Thrown at execution/executor/src/lib.rs:323

                    .collect(),
                &proof_reader,
            )
            .expect("Failed to update state tree.");

        for ((vm_output, txn), ((state_tree_hash, new_node_hashes), blobs)) in itertools::zip_eq(
            itertools::zip_eq(vm_outputs.into_iter(), transactions.iter()).take(transaction_count),
            itertools::zip_eq(roots_with_node_hashes, txn_blobs),
        ) {
            let event_tree = {
                let event_hashes: Vec<_> =
                    vm_output.events().iter().map(CryptoHash::hash).collect();
                InMemoryAccumulator::<EventAccumulatorHasher>::from_leaves(&event_hashes)
            };

            let mut txn_info_hash = None;
            match vm_output.status() {
                TransactionStatus::Keep(status) => {
                    ensure!(
                        !vm_output.write_set().is_empty(),
                        "Transaction with empty write set should be discarded.",
                    );
                    // Compute hash for the TransactionInfo object. We need the hash of the
                    // transaction itself, the state root hash as well as the event root hash.
                    let txn_info = TransactionInfo::new(
                        txn.hash(),
                        state_tree_hash,
                        event_tree.root_hash(),
                        vm_output.gas_used(),
                        status.clone(),
                    );

                    let real_txn_info_hash = txn_info.hash();
                    txn_info_hashes.push(real_txn_info_hash);
                    txn_info_hash = Some(real_txn_info_hash);
                }
                TransactionStatus::Discard(status) => {

View on GitHub (pinned to fc4714a8ea)

Solutions

  1. Fix the transaction itself so it performs a state write if it should be committed (non-trivial script/program), or change its semantics to be discarded.
  2. Check the VM adapter: if the transaction is legitimately a no-op, the adapter should map it to TransactionStatus::Discard instead of Keep before calling process_vm_outputs.
  3. If this comes from replaying a synced chunk, verify the upstream output data is not corrupted/truncated (write set lost during serialization).
  4. Ensure the executor and VM versions are consistent — rebuild/redeploy both from the same revision.

Example fix

// before: adapter marks a no-op transaction as Keep
Ok(VMStatus::Executed) // output.write_set() empty
// after: discard no-op outputs instead of committing them
if output.write_set().is_empty() {
    return Ok(TransactionStatus::Discard(DiscountedVMStatus::MiscellaneousError));
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Before feeding VM outputs into the commit pipeline, ensure Keep outputs write state.
for output in &vm_outputs {
    if matches!(output.status(), TransactionStatus::Keep(_))
        && output.write_set().is_empty() {
        anyhow::bail!("output marked Keep has empty write set; adapter bug?");
    }
}

Type guard

fn committable(output: &VMOutput) -> bool {
    !matches!(output.status(), TransactionStatus::Keep(_))
        || !output.write_set().is_empty()
}

Try / catch

match executor.execute_chunk(txns, proof, &ledger_info) {
    Err(e) if e.to_string().contains("empty write set") => {
        // treat as adapter/VM bug: drop the offending tx, alert, do not retry blindly
        report_invariant_violation(&e);
        Err(e)
    }
    other => other,
}

Prevention

When it happens

Trigger: The Move VM returns TransactionStatus::Keep but vm_output.write_set() is empty — typically caused by a buggy adapter/script producing a committed-but-no-op output, or a malicious/incorrect VM output being fed into process_vm_outputs during chunk execution.

Common situations: Custom or modified Move VM adapters/transaction pipelines in tests producing empty outputs; a VM version mismatch (upgraded VM no longer emits writes for a transaction that previously did); mis-wired execution path passing outputs from discarded or metadata transactions as regular Keep transactions.

Related errors


AI-assisted analysis of diem/diem@fc4714a8ea (2026-09-04). Data as JSON: /api/errors/2daf8b0a0d80d4d1. Report an issue: GitHub.