Skip to content

derive cell level masks instead of storing them - #502

Merged
alpinskiy-top merged 1 commit into
masterfrom
malpinskiy/work
Aug 31, 2026
Merged

derive cell level masks instead of storing them#502
alpinskiy-top merged 1 commit into
masterfrom
malpinskiy/work

Conversation

@alpinskiy-top

Copy link
Copy Markdown
Contributor

boc.Cell.mask was only ever populated by the deserializer (from the d1 byte) and hand-set once in pruneCells. Nothing else maintained it: NewCell leaves it at 0 and AddRef never ORs a child's mask into the parent, so any tree built in memory around a nonzero-level cell - everything tlb.Marshal produces, e.g. via txemulator.tlbStructToBase64 - serialized as a level 0 parent over a level 1 child. ton rejects that with "level mask mismatch". The same stale mask made newImmutableCell compute the wrong number of hashes, so Hash256() of a freshly built tree disagreed with the identical wire-decoded one.

Drop the field and derive the mask from a cell's type, content and refs the way ton's DataCell::create does: ordinary is the union of its refs, pruned branch takes its mask from the data, library is 0, and merkle cells shift right - a plain union would give a merkle proof root the wrong level. Both consumers go through the one rule, so serialization and hashing can no longer drift apart.

Deriving a pruned branch's mask also validates its body length, which closes an out of range read in immutableCell.Hash/Depth reachable from any boc.

Hash256() keeps returning the hash of the highest level, matching ton's Cell::get_hash(). For a tree with pruned branches that is no longer the representation hash, so HashAtLevel is added to get it.

boc.Cell.mask was only ever populated by the deserializer (from the d1 byte)
and hand-set once in pruneCells. Nothing else maintained it: NewCell leaves it
at 0 and AddRef never ORs a child's mask into the parent, so any tree built in
memory around a nonzero-level cell - everything tlb.Marshal produces, e.g. via
txemulator.tlbStructToBase64 - serialized as a level 0 parent over a level 1
child. ton rejects that with "level mask mismatch". The same stale mask made
newImmutableCell compute the wrong number of hashes, so Hash256() of a freshly
built tree disagreed with the identical wire-decoded one.

Drop the field and derive the mask from a cell's type, content and refs the way
ton's DataCell::create does: ordinary is the union of its refs, pruned branch
takes its mask from the data, library is 0, and merkle cells shift right - a
plain union would give a merkle proof root the wrong level. Both consumers go
through the one rule, so serialization and hashing can no longer drift apart.

Deriving a pruned branch's mask also validates its body length, which closes an
out of range read in immutableCell.Hash/Depth reachable from any boc.

Hash256() keeps returning the hash of the highest level, matching ton's
Cell::get_hash(). For a tree with pruned branches that is no longer the
representation hash, so HashAtLevel is added to get it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@alpinskiy-top
alpinskiy-top merged commit 4756ac3 into master Aug 31, 2026
2 checks passed
@alpinskiy-top
alpinskiy-top deleted the malpinskiy/work branch August 31, 2026 07:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant