DEV Community

Tosh
Tosh

Posted on

Decoding Error 1010: What 'Invalid Transaction' Actually Means

Decoding Error 1010: What 'Invalid Transaction' Actually Means

Error 1010 on Midnight Network is the kind of thing that stops you cold. You've built the circuit, written the TypeScript driver, deployed to testnet — and then: InvalidTransaction. No stack trace. No obvious cause. Just a number.

Here's what's actually happening.

It Isn't Midnight's Error

This trips people up because they go digging in Midnight-specific docs first. Error 1010 isn't a custom Midnight error code. It's Substrate's InvalidTransaction::ExhaustsResources — code 10 — surfaced through the POOL_INVALID_TX mechanism after Midnight's own validation layer rejects the transaction.

The arithmetic is straightforward once you see it:

POOL_INVALID_TX = AUTHOR(1000) + offset
Error 1010 = 1000 + 10
Enter fullscreen mode Exit fullscreen mode

AUTHOR(1000) is the base code for pool-level transaction invalidity. The offset (10) maps to a specific reason. So 1010 means: the transaction pool rejected this as invalid because it exhausts block resources.

What this tells you diagnostically: the transaction is structurally valid (it parsed, it was submitted) but the node calculated its resource footprint and found it exceeds what the block can accommodate. This is a cost model problem, not a cryptographic or logic problem.

The Cost Model: Five Dimensions

To understand why your transaction gets rejected, you need to understand what the node is actually measuring. The Midnight ledger cost model has five dimensions:

1. readTime — How long it takes to read ledger state required by the transaction. Transactions that touch many ledger keys or deep Merkle trees drive this up.

2. computeTime — The computational weight of executing ZK proof verification and contract logic. More circuits, larger proofs, more complex ledger operations all contribute here.

3. blockUsage — A normalized measure of the total block capacity this transaction consumes relative to the block limit.

4. bytesWritten — Bytes of new state written to the ledger. New contract deployments and state transitions that create new ledger entries hit this dimension.

5. bytesChurned — Bytes of existing state modified or deleted. Heavy state updates, batch operations, and Merkle tree restructuring show up here.

Each dimension gets mapped to a Substrate weight. If any single dimension causes the combined weight to exceed the block capacity, Substrate rejects with ExhaustsResources. You don't get a breakdown telling you which dimension failed — you just get 1010.

That's the frustrating part. The diagnostic work is reconstructing which dimension is the bottleneck.

Common Error Codes and What They Mean

The 1010 error exposes a broader pattern in how Midnight surfaces transaction failures. The offset system extends beyond just resource exhaustion. Understanding the full set gives you a faster path from error to fix.

Error 139 — Transaction Builder

Error 139 surfaces from the TypeScript-side transaction builder before the transaction even hits the node. When I first saw this one, I wasted 20 minutes looking at my circuit. The problem was in the driver.

The transaction builder validates inputs, outputs, and proof references during construction. Code 139 means something in that assembly process failed — mismatched proof references, incorrect witness data structure, or a type mismatch between what the circuit expects and what the builder was given.

Diagnostic steps:

  • Check that your CallTx or DeployTx builder calls match the current contract API
  • Verify witness function return types match the circuit's declared parameter types
  • If you recently upgraded the @midnight-ntwrk packages, check for breaking changes in builder APIs
  • Look at the exact field reported in the error — the TypeScript SDK typically names the problematic property
try {
  const tx = await contract.callTx.myCircuit(witnessProvider);
  await txSub.submit(tx, { slotOffset: 1n });
} catch (e) {
  if (e instanceof CallTxFailedError) {
    console.error('Builder error code:', e.code);
    console.error('Failed at:', e.message);
  }
}
Enter fullscreen mode Exit fullscreen mode

Error 154 — BlockLimitExceeded

This is the direct cousin of error 1010. Where 1010 is Substrate's rejection from the pool, 154 is Midnight's own cost model telling you a transaction exceeds block limits before submission even completes.

The difference matters. A 154 error means you can catch this client-side without burning a transaction fee. The node calculated the cost and refused to even put it in the pool.

This one hits most often on initial contract deployments with many circuits. Deploying a contract that includes five or six complex circuits in a single transaction can easily exceed bytesWritten limits.

Diagnostic steps:

  • Use initialTransactionCostModel() from @midnight-ntwrk/ledger to get current limits
  • If deploying a large contract, split circuits across multiple deployment transactions
  • Check which of the five cost dimensions is the bottleneck by estimating each independently
  • Monitor bytesWritten carefully — new key creation is expensive
import { TransactionCostModel } from '@midnight-ntwrk/ledger';

const costModel = TransactionCostModel.initialTransactionCostModel();
console.log('Current cost model:', costModel.toString());
// Inspect inputFeeOverhead and outputFeeOverhead to calibrate
Enter fullscreen mode Exit fullscreen mode

Error 168 — Batch Settlement

Error 168 appears in contexts involving batch operations — multiple state transitions settled in a single transaction. The batch settlement mechanism has its own validation layer that runs after proof verification but before final ledger state update.

Batch settlement errors usually mean one of two things: the batch was assembled with conflicting state assumptions (two operations in the batch both expect to write to the same ledger key in incompatible ways), or the aggregate cost of the batch exceeds per-transaction limits.

Diagnostic steps:

  • If running a batch, reduce the batch size and resubmit
  • Check for state key conflicts within the batch — operations that might race on the same ledger location
  • Verify that the ledger state at submission time matches what the proofs were generated against; state staleness is a common cause

Error 170 — Merkle Root Pruning

Midnight uses Merkle trees for both membership proofs and historical state. Error 170 means a transaction referenced a Merkle root that has been pruned from the node's state — the proof was generated against a historical snapshot the node no longer holds.

This is a timing problem, not a logic problem.

// Don't generate the proof too far in advance of submission
// The window between proof generation and submission matters

const proof = await prover.prove(circuit, inputs);
// Submit promptly — don't cache proofs for later use
await txSub.submit(proof, { slotOffset: 1n });
Enter fullscreen mode Exit fullscreen mode

Diagnostic steps:

  • Reduce the time between proof generation and transaction submission
  • If using cached proofs, regenerate before each submission attempt
  • Check the node's current slot height against when the Merkle root was generated
  • On testnet, the pruning window may be shorter than expected — regenerate more frequently than you would on mainnet

Error 186 — EffectsCheckFailure

Error 186 is the most opaque of the set. It means the transaction's declared effects — what it claims to do to ledger state — don't match what execution actually produces.

Every Midnight transaction includes a declaration of its effects before proof verification happens. If the declared effects and the actual computed effects diverge, the node rejects with 186. This is a security property, not a runtime bug you can patch around.

In practice, this surfaces when:

  • A witness function returns unexpected data that causes the circuit to compute different state transitions than what was declared
  • The ledger state changed between when you generated the effect declaration and when the transaction was submitted
  • There's a mismatch between the circuit's compiled version and what the runtime is executing against

Diagnostic steps:

  • Verify the compiled circuit hash matches what the contract was deployed with
  • Check that witness functions return stable, deterministic data
  • Add logging to your witness implementations to confirm they return exactly what you expect
  • If state can change between effect declaration and submission, add a validation step before submitting

A Debugging Heuristic

Most error 1010 / 154 issues I've seen come down to large deployments hitting bytesWritten. The fix is splitting deployments. For everything else, here's a quick triage order:

  1. Is it 139? — Transaction builder, check your TypeScript driver.
  2. Is it 154 or 1010? — Cost model, look at bytesWritten and circuit count.
  3. Is it 168? — Batch conflict or size, reduce and retry.
  4. Is it 170? — Timing, regenerate your proof.
  5. Is it 186? — Effect mismatch, check witness determinism and circuit version.

The 1010 surface error is always one layer removed from the actual cause. The actual cause is always in that list.

Working with the Cost Model in Practice

The TransactionCostModel.initialTransactionCostModel() call gives you current limits but not per-dimension breakdowns in a usable format yet. The tooling for pre-flight cost estimation is still developing.

What works now:

import { TransactionCostModel } from '@midnight-ntwrk/ledger';

async function estimateAndSubmit(tx: Transaction) {
  const costModel = TransactionCostModel.initialTransactionCostModel();

  // Log baseline to compare across environments
  console.log('Baseline cost:', costModel.baselineCost);
  console.log('Input overhead:', costModel.inputFeeOverhead);
  console.log('Output overhead:', costModel.outputFeeOverhead);

  try {
    await txSub.submit(tx, { slotOffset: 1n });
  } catch (e) {
    // Map the error code to a dimension
    console.error('Transaction rejected, cost model limits hit');
    throw e;
  }
}
Enter fullscreen mode Exit fullscreen mode

The honest answer is: right now, you find out which dimension you're hitting by trial and error — reducing circuit count, splitting deployments, and watching which change fixes the rejection. The forum thread by nel349 (February 2026) is still open waiting for official tooling guidance from the Compact team.

The Actual Mental Model

Error 1010 means: Substrate weighed your transaction against its five-dimensional block capacity check and found it too heavy. The fix is almost always reducing the size of what you're doing — fewer circuits per deployment, smaller batches, fresher proofs.

The offset arithmetic (AUTHOR(1000) + offset) is worth internalizing. Once you know 1010 = pool rejection + resources exhausted, the diagnostic path becomes obvious. Work backwards from the weight that failed to the dimension that's over limit to the operation that created the overage.

It's actually a sensible error system. It just doesn't explain itself well the first time you hit it at 2am with a deployment deadline.

Top comments (0)