Skip to content

Transaction builder underestimates minimum cell capacity #364

@Radiiplus

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions