DEV Community

Tosh
Tosh

Posted on

Decoding Error 1010: What 'Invalid Transaction' Actually Means on Midnight

Decoding Error 1010: What 'Invalid Transaction' Actually Means

You've deployed to Midnight testnet. The circuit compiles. The TypeScript driver looks right. And then: InvalidTransaction. No stack trace. No line number. Just the number 1010 staring back at you.

I spent four hours debugging this the first time. The fix took two lines of code. Here's what I learned.


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.

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.

Error 139 — Transaction Builder

Error 139 surfaces from the TypeScript-side transaction builder before the transaction even hits the node. 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.

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. A 154 error means you can catch this client-side without burning a transaction fee.

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

const costModel = TransactionCostModel.initialTransactionCostModel();
console.log('Current cost model:', costModel.toString());
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. Batch settlement errors usually mean one of two things: the batch was assembled with conflicting state assumptions, or the aggregate cost exceeds per-transaction limits.

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.

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

Error 186 — EffectsCheckFailure

Error 186 means the transaction's declared effects — what it claims to do to ledger state — don't match what execution actually produces. This is a security property, not something you can patch around.

A Debugging Heuristic

Most error 1010 / 154 issues come down to large deployments hitting bytesWritten. The fix is splitting deployments. For everything else:

  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.

Working with the Cost Model in Practice

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

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

  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) {
    console.error('Transaction rejected, cost model limits hit');
    throw e;
  }
}
Enter fullscreen mode Exit fullscreen mode

The honest answer: right now, you find out which dimension you're hitting by trial and error. 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.

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.


Built with AI? I build autonomous revenue agents. Follow for more.

Top comments (0)