LusoCryptoLabs

Field notes

What running it
actually teaches.

One rule: every entry carries the number it was measured at.

These are not opinions about chains. They are things that were true on a particular day, against a real node, usually discovered because something failed in a way that said nothing useful about why.

Where a finding belonged to somebody else's project, the report is linked at the bottom of the entry. Where we found something and have not taken it anywhere, that is said too.

2026-09-12Nervos CKB

We read a stale config and called it a broken library. It was not

This entry said something wrong for a day, and the correction is worth more than the claim was.

CCC is the library most Nervos applications build transactions with. For JoyID on mainnet it ships five fixed cell deps, and we checked them against the chain: all five outpoints are spent, in 1.12.5 and in 1.20.1. That part is true and still is. We concluded a mainnet transaction needing that lock would be rejected, and reported it.

the static config 0x8a605a44…:0-4 all spent what the tx builder puts in 0x673e1843…:0-4 all live

The maintainer's reply was that getCellDeps resolves each dep by its type id before use, so the stale outpoint never reaches a transaction. We tested the path an application actually takes: the JoyID signer's prepareTransaction calls addCellDepsOfKnownScripts, which resolves, and the built transaction carries the live cells. He was right. Nothing is broken, and the fix we had written for ourselves was redundant.

The lesson is the one this page exists for. We verified a value and not a path. A number that is stale in a config file is only a bug if something uses it as written, and checking whether anything does is one more step than we took. The reported issue stays open as a documentation request, which is what it should have been.

Reported, then corrected: ckb-devrel/ccc#531

2026-09-12Nervos CKB

A contract cannot verify a witness of 64 KiB, and the error says nothing about size

We were sizing a second image for an on-chain profile and wanted to know the real ceiling. A type script loads its witness through one allocation, and does it more than once per run. The allocator rounds each request up to a power of two, so the moment a witness crosses 64 KiB every one of those allocations doubles and the heap runs out. The script exits -1.

witness 64,593 B -> ACCEPTED witness 65,617 B -> Inputs[0].Type, error code -1

The boundary is 65,536 exactly. Nothing in the error mentions bytes, so from the outside it looks like a contract bug rather than a budget.

2026-09-12Method

A limit measured without running the scripts is not a limit

Our own SDK had carried a 500 KB ceiling for months, derived honestly: a script built transactions of increasing size and measured them against what fits in a block. Its header even said so, that nothing was signed or sent.

That measures the block. The block is not the first wall. Asking a node to actually run the scripts put the real ceiling at 52 KB, nine times lower, and the failure above it is a bare -1. The check had never been wrong; it had been answering a different question than the one everybody read it as answering.

believed 500 KB (transactions built and measured) actual 52 KB (test_tx_pool_accept, scripts run)
2026-09-12CKBFS

Two ceilings stack when you publish a file on chain, and neither is documented

CKBFS stores files in transaction witnesses. Publishing one runs into two separate walls, found with a node's cycle estimator so they cost nothing to establish:

the contract: at most 8 content witnesses per transaction (a count, not a size: 8 x 20 KB passes, 9 x 20 KB fails) the lock: a signature over a witness much above 32 KB fails, error -22 in practice: 240,000 bytes per publish transaction

Both surface as bare error codes. We also found that a conforming reader has to check the checksum each backlink declares for everything published before it, not only the final sum, or a spliced history reads as valid.

Reported: code-monad/ckbfs#3

2026-09-12Nervos CKB

A comment changes the bytes that are on the chain

Rust contracts compiled with overflow checks put a panic location, file and line number, at every bounds check. Move a line in a shared crate and every contract that links it comes out byte-different, with no change to its logic and none to its own source.

one comment added to a shared crate -> 1 byte different in 39,888, at offset 0xdc0 -> the line number 625 became 626

The cost is not the byte. It is that verifying a deployed contract against the current source then reports a mismatch for a comment, which trains whoever reads that output to shrug at a red light. We changed the question the tooling asks: not "does this match what the source builds today" but "does this match what the commit it was built from builds".

2026-09-12Security

A content policy that reads as careful can break every page it protects

Serving a document somebody else wrote, from a host of yours, is the one place a strict policy earns its keep. Ours went out as sandbox allow-scripts; default-src 'self' data:, which looks responsible and silently discarded the page's own inline style. The first render was unstyled serif text, with nothing logged anywhere.

A single-file page stored on a chain is inline CSS almost by definition, so that policy does not harden such pages, it breaks all of them. The sandbox is what does the security work; the source directives decide what the page may load. Getting that backwards costs you the page and buys nothing.

2026-09-12Ecosystem

Before you build on it, count its transactions

We chose an on-chain file protocol over IPFS and Arweave deliberately, and then went to check what we were standing on. On mainnet, that protocol's code hash has three transactions. One team, a community proposal rather than a ratified standard, repositories at about one star.

That did not change the decision, because the exposure is bounded: the reader is ours, and the bytes sit in the chain's history, so reading keeps working even if the project stops. What it changed is that the risk is written down where the next person will read it, instead of being discovered by them later.

StandingHonest note

Most of what we learn is still not shared

The two reports above were both filed on the same day, which tells you the true state of this: we have used this ecosystem heavily for two years, and until now gave almost nothing back, because what we learned lived in private repositories.

That is the gap this page exists to close, and a page is not the whole of closing it. The measurements above are public now. The readers, the tooling and the patches behind them mostly are not, yet.