Skip to content

Transaction builder underestimates minimum cell capacity #364

Description

@Radiiplus

Description
The CCC library's CellOutput.from() auto-capacity calculation only triggers when capacity === 0, but when a developer provides a non-zero capacity that's still too low, the library silently accepts it and the node rejects the transaction with InsufficientCellCapacity.

Error

InsufficientCellCapacity(Outputs[0]): expected occupied capacity (0x58d5e200) <= capacity (0x2540be400)

Root Cause

In @ckb-ccc/core/src/ckb/transaction.ts (lines 260-282), CellOutput.from() auto-calculates capacity:

if (output.capacity === Zero && outputData != null) {
  output.capacity = fixedPointFrom(
    output.occupiedSize + bytesFrom(outputData).length,
  );
}

This only runs when capacity === 0. If a developer sets any non-zero value (even if too low), the library skips the calculation and passes the transaction through. The node then rejects it.

The actual occupied size formula (CellOutput.occupiedSize at line 232-233) is:

get occupiedSize(): number {
  return 8 + this.lock.occupiedSize + (this.type?.occupiedSize ?? 0);
}

Where Script.occupiedSize (src/ckb/script.ts line 134) is:

get occupiedSize(): number {
  return 33 + bytesFrom(this.args).length;
}

So the true minimum is: 8 + (33 + args) + data — but developers only account for data.

Reproduction

const outputs = [{
  lock: lockScript,
  capacity: 256n * 100_000_000n, // 256 CKB for 256 bytes of data
}];

await txBuilder.buildAndSend(outputs, [dataHex], privateKey);
// Fails: InsufficientCellCapacity
// Actual required: (8 + 33 + 20 + 256) = 317 CKB minimum

Expected Behavior

The library should validate capacity in CellOutput.from() regardless of whether it's zero or non-zero, and throw a clear error like:

Insufficient capacity: provided 256 CKB, minimum required 317 CKB 
(8 struct + 53 lock + 256 data = 317 bytes)

Impact

  • The auto-calculation exists but only triggers on capacity === 0
  • Any non-zero under-estimated value bypasses the check
  • Developers consistently hit this because the common "1 CKB = 1 byte" mental model ignores the ~61 bytes of struct + script overhead
  • The common workaround is hardcoding 500+ CKB buffers, wasting ~28% per cell or manually calculating it which you cant do because as a developer new to the system, i definitly wont account for those overheads.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions