{"record":{"id":"2daf8b0a0d80d4d1","repo":"diem/diem","slug":"transaction-with-empty-write-set-should-be-discard","errorCode":null,"errorMessage":"Transaction with empty write set should be discarded.","messagePattern":"Transaction with empty write set should be discarded\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"execution/executor/src/lib.rs","lineNumber":323,"sourceCode":"                    .collect(),\n                &proof_reader,\n            )\n            .expect(\"Failed to update state tree.\");\n\n        for ((vm_output, txn), ((state_tree_hash, new_node_hashes), blobs)) in itertools::zip_eq(\n            itertools::zip_eq(vm_outputs.into_iter(), transactions.iter()).take(transaction_count),\n            itertools::zip_eq(roots_with_node_hashes, txn_blobs),\n        ) {\n            let event_tree = {\n                let event_hashes: Vec<_> =\n                    vm_output.events().iter().map(CryptoHash::hash).collect();\n                InMemoryAccumulator::<EventAccumulatorHasher>::from_leaves(&event_hashes)\n            };\n\n            let mut txn_info_hash = None;\n            match vm_output.status() {\n                TransactionStatus::Keep(status) => {\n                    ensure!(\n                        !vm_output.write_set().is_empty(),\n                        \"Transaction with empty write set should be discarded.\",\n                    );\n                    // Compute hash for the TransactionInfo object. We need the hash of the\n                    // transaction itself, the state root hash as well as the event root hash.\n                    let txn_info = TransactionInfo::new(\n                        txn.hash(),\n                        state_tree_hash,\n                        event_tree.root_hash(),\n                        vm_output.gas_used(),\n                        status.clone(),\n                    );\n\n                    let real_txn_info_hash = txn_info.hash();\n                    txn_info_hashes.push(real_txn_info_hash);\n                    txn_info_hash = Some(real_txn_info_hash);\n                }\n                TransactionStatus::Discard(status) => {","sourceCodeStart":305,"sourceCodeEnd":341,"githubUrl":"https://github.com/diem/diem/blob/fc4714a8ea273b6efe8b13dbce72ea60aad9a16c/execution/executor/src/lib.rs#L305-L341","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","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.","If this comes from replaying a synced chunk, verify the upstream output data is not corrupted/truncated (write set lost during serialization).","Ensure the executor and VM versions are consistent — rebuild/redeploy both from the same revision."],"exampleFix":"// before: adapter marks a no-op transaction as Keep\nOk(VMStatus::Executed) // output.write_set() empty\n// after: discard no-op outputs instead of committing them\nif output.write_set().is_empty() {\n    return Ok(TransactionStatus::Discard(DiscountedVMStatus::MiscellaneousError));\n}","handlingStrategy":"try-catch","validationCode":"// Before feeding VM outputs into the commit pipeline, ensure Keep outputs write state.\nfor output in &vm_outputs {\n    if matches!(output.status(), TransactionStatus::Keep(_))\n        && output.write_set().is_empty() {\n        anyhow::bail!(\"output marked Keep has empty write set; adapter bug?\");\n    }\n}","typeGuard":"fn committable(output: &VMOutput) -> bool {\n    !matches!(output.status(), TransactionStatus::Keep(_))\n        || !output.write_set().is_empty()\n}","tryCatchPattern":"match executor.execute_chunk(txns, proof, &ledger_info) {\n    Err(e) if e.to_string().contains(\"empty write set\") => {\n        // treat as adapter/VM bug: drop the offending tx, alert, do not retry blindly\n        report_invariant_violation(&e);\n        Err(e)\n    }\n    other => other,\n}","preventionTips":["Ensure the VM adapter maps no-op transactions to Discard, not Keep.","Keep VM and executor on matching versions; gas/status changes can alter write sets.","Add unit tests asserting every Keep output has a non-empty write set.","Never hand-construct VMOutput values in tests without a write set when status is Keep."],"tags":["move-vm","transaction-execution","invariant-violation","blockchain"],"backgroundTag":"empty-write-set-transaction","analyzedSha":"fc4714a8ea273b6efe8b13dbce72ea60aad9a16c","analyzedAt":"2026-09-04T21:07:05.890Z","contentChangedAt":"2026-09-04T21:07:05.890Z","schemaVersion":2},"datasetVersion":"2026-09-12T02:17:10.037Z"}