Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions boc/cell.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ func (c *Cell) NextRef() (*Cell, error) {
ref := c.refs[c.refCursor]
if ref != nil {
c.refCursor++
ref.ResetCounters()
ref.ShallowResetCounters()
return ref, nil
}
return nil, ErrNotEnoughRefs
Expand All @@ -211,7 +211,7 @@ func (c *Cell) NextRefV() (Cell, error) {
ref := c.refs[c.refCursor]
if ref != nil {
c.refCursor++
ref.ResetCounters()
ref.ShallowResetCounters()
return *ref, nil
}
return Cell{}, ErrNotEnoughRefs
Expand Down Expand Up @@ -529,15 +529,32 @@ func (c *Cell) WriteBytes(b []byte) error {
}

func (c *Cell) ResetCounters() {
c.resetCounters(map[*Cell]struct{}{})
}

func (c *Cell) resetCounters(visited map[*Cell]struct{}) {
// "visited" dedups shared cells by pointer identity.
// Without this the recursion re-walks shared subtrees
// once per path through them — exponential on real code cells.
// Mirrors (*Cell).hash, which already memoizes by *Cell.
if _, ok := visited[c]; ok {
return
}
visited[c] = struct{}{}
c.bits.ResetCounter()
c.refCursor = 0
for _, r := range c.refs {
if r != nil {
r.ResetCounters()
r.resetCounters(visited)
}
}
}

func (c *Cell) ShallowResetCounters() {
c.bits.ResetCounter()
c.refCursor = 0
}

func (c *Cell) BitsAvailableForRead() int {
return c.bits.BitsAvailableForRead()
}
Expand Down
58 changes: 58 additions & 0 deletions boc/cell_nextref_reset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package boc

import "testing"

func TestNextRefShallowReset(t *testing.T) {
// root -> child -> grandchild, each carrying 8 readable bits.
grandchild := NewCell()
if err := grandchild.WriteUint(0xAA, 8); err != nil {
t.Fatal(err)
}
child := NewCell()
if err := child.WriteUint(0xBB, 8); err != nil {
t.Fatal(err)
}
if err := child.AddRef(grandchild); err != nil {
t.Fatal(err)
}
root := NewCell()
if err := root.AddRef(child); err != nil {
t.Fatal(err)
}
// Advance the read cursors of child and grandchild so a reset is observable.
if _, err := child.ReadUint(8); err != nil {
t.Fatal(err)
}
if _, err := grandchild.ReadUint(8); err != nil {
t.Fatal(err)
}
if child.BitsAvailableForRead() != 0 || grandchild.BitsAvailableForRead() != 0 {
t.Fatalf("setup: expected both cursors exhausted")
}
// Descend into child: its own cursor must be reset
got, err := root.NextRef()
if err != nil {
t.Fatal(err)
}
if got != child {
t.Fatal("NextRef returned wrong cell")
}
if child.BitsAvailableForRead() != 8 {
t.Fatalf("child cursor not reset: got %d want 8", child.BitsAvailableForRead())
}
// but the grandchild must NOT have been touched (shallow reset).
if grandchild.BitsAvailableForRead() != 0 {
t.Fatalf("grandchild was reset by NextRef into child — reset is not shallow (the regression)")
}
// Correctness preserved: descending into the grandchild resets it then.
gotGC, err := child.NextRef()
if err != nil {
t.Fatal(err)
}
if gotGC != grandchild {
t.Fatal("NextRef returned wrong grandchild")
}
if grandchild.BitsAvailableForRead() != 8 {
t.Fatalf("grandchild cursor not reset when descended into: got %d want 8", grandchild.BitsAvailableForRead())
}
}
32 changes: 32 additions & 0 deletions boc/cell_resetcounters_dag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package boc

import (
"testing"
"time"
)

func TestResetCountersDAGNoExponentialBlowup(t *testing.T) {
const depth = 60
cells := make([]*Cell, depth+1)
cells[depth] = NewCell()
for i := depth - 1; i >= 0; i-- {
c := NewCell()
if err := c.AddRef(cells[i+1]); err != nil {
t.Fatalf("AddRef: %v", err)
}
if err := c.AddRef(cells[i+1]); err != nil {
t.Fatalf("AddRef: %v", err)
}
cells[i] = c
}
done := make(chan struct{})
go func() {
defer close(done)
cells[0].ResetCounters()
}()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("ResetCounters did not terminate — exponential DAG blowup regressed")
}
}
39 changes: 18 additions & 21 deletions code/libraries.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import (
// FindLibraries looks for library cells inside the given cell tree and
// returns a list of hashes of found library cells.
func FindLibraries(cell *boc.Cell) ([]ton.Bits256, error) {
libs, err := findLibraries(cell)
if err != nil {
libs := make(map[ton.Bits256]struct{})
visited := make(map[*boc.Cell]struct{})
if err := findLibraries(cell, libs, visited); err != nil {
return nil, err
}
if len(libs) == 0 {
Expand All @@ -23,37 +24,33 @@ func FindLibraries(cell *boc.Cell) ([]ton.Bits256, error) {
return hashes, nil
}

func findLibraries(cell *boc.Cell) (map[ton.Bits256]struct{}, error) {
func findLibraries(cell *boc.Cell, libs map[ton.Bits256]struct{}, visited map[*boc.Cell]struct{}) error {
// "visited" tracks cells already walked by pointer identity.
// Without this set, findLibraries explores every root->leaf
// path independently, which is exponential on real code cells.
if _, ok := visited[cell]; ok {
return nil
}
visited[cell] = struct{}{}
cell.ShallowResetCounters()
if cell.IsExotic() {
if cell.CellType() == boc.LibraryCell {
bytes, err := cell.ReadBytes(33)
if err != nil {
return nil, err
return err
}
var hash ton.Bits256
copy(hash[:], bytes[1:])
return map[ton.Bits256]struct{}{
hash: {},
}, nil
libs[hash] = struct{}{}
}
return nil, nil
return nil
}
var libs map[ton.Bits256]struct{}
for _, ref := range cell.Refs() {
ref.ResetCounters()
hashes, err := findLibraries(ref)
if err != nil {
return nil, err
}
if libs == nil {
libs = hashes
continue
}
for hash := range hashes {
libs[hash] = struct{}{}
if err := findLibraries(ref, libs, visited); err != nil {
return err
}
}
return libs, nil
return nil
}

// LibrariesToBase64 converts a map with libraries to a base64 string.
Expand Down
40 changes: 40 additions & 0 deletions code/libraries_dag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package code

import (
"testing"
"time"

"github.com/tonkeeper/tongo/boc"
)

func TestFindLibrariesDAGNoExponentialBlowup(t *testing.T) {
const depth = 60
cells := make([]*boc.Cell, depth+1)
cells[depth] = boc.NewCell()
for i := depth - 1; i >= 0; i-- {
c := boc.NewCell()
if err := c.AddRef(cells[i+1]); err != nil {
t.Fatalf("AddRef: %v", err)
}
if err := c.AddRef(cells[i+1]); err != nil {
t.Fatalf("AddRef: %v", err)
}
cells[i] = c
}
done := make(chan struct{})
go func() {
defer close(done)
libs, err := FindLibraries(cells[0])
if err != nil {
t.Errorf("FindLibraries: %v", err)
}
if len(libs) != 0 {
t.Errorf("expected no libraries, got %d", len(libs))
}
}()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("FindLibraries did not terminate — exponential DAG blowup regressed")
}
}
2 changes: 1 addition & 1 deletion tlb/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (m *Message) UnmarshalTLB(c *boc.Cell, decoder *Decoder) error {
return err
}
copy(m.hash[:], hash[:])
c.ResetCounters()
c.ShallowResetCounters()

var msg struct {
Info CommonMsgInfo
Expand Down
2 changes: 1 addition & 1 deletion tlb/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (tx *Transaction) UnmarshalTLB(c *boc.Cell, decoder *Decoder) error {
return err
}
copy(tx.hash[:], hash[:])
c.ResetCounters()
c.ShallowResetCounters()

sumType, err := c.ReadUint(4)
if err != nil {
Expand Down
66 changes: 34 additions & 32 deletions tychoclient/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package tychoclient
import (
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
"os"
"testing"
Expand Down Expand Up @@ -298,62 +299,55 @@ func TestParseTychoBlockErrorCases(t *testing.T) {
func TestParseShardAccount(t *testing.T) {
tests := []struct {
name string
bocData []byte
proof []byte
expectError bool
errorMsg string
}{
{
name: "empty BOC data",
bocData: []byte{},
name: "empty proof data",
proof: []byte{},
expectError: true,
errorMsg: "empty BOC data",
errorMsg: "failed to decode account data from proof",
},
{
name: "nil BOC data",
bocData: nil,
name: "nil proof data",
proof: nil,
expectError: true,
errorMsg: "empty BOC data",
errorMsg: "failed to decode account data from proof",
},
{
name: "invalid BOC data",
bocData: []byte{0x01, 0x02, 0x03},
name: "invalid proof data",
proof: []byte{0x01, 0x02, 0x03},
expectError: true,
errorMsg: "failed to deserialize BOC",
errorMsg: "failed to decode account data from proof",
},
{
name: "short invalid BOC data",
bocData: []byte{0xb5, 0xee},
name: "short invalid proof data",
proof: []byte{0xb5, 0xee},
expectError: true,
errorMsg: "failed to deserialize BOC",
errorMsg: "failed to decode account data from proof",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
account, err := ParseShardAccount(tt.bocData)
_, _, err := ParseShardAccount(nil, tt.proof, nil)

if tt.expectError {
if err == nil {
t.Errorf("expected error but got none")
return
}
if tt.errorMsg != "" {
if len(err.Error()) == 0 || err.Error()[:len(tt.errorMsg)] != tt.errorMsg {
if len(err.Error()) < len(tt.errorMsg) || err.Error()[:len(tt.errorMsg)] != tt.errorMsg {
t.Errorf("expected error to contain '%s', got: %v", tt.errorMsg, err)
}
}
if account != nil {
t.Errorf("expected nil account on error, got: %v", account)
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
if account == nil {
t.Error("expected account but got nil")
return
}
}
})
}
Expand Down Expand Up @@ -459,11 +453,23 @@ func TestParseShardAccount_Integration(t *testing.T) {
t.Error("Empty BOC data after decoding")
}

// Decode the proof and account address required for parsing
var proofData []byte
if len(fixture.Proof) > 0 {
proofData, err = base64.StdEncoding.DecodeString(fixture.Proof)
if err != nil {
t.Fatalf("Failed to decode proof: %v", err)
}
}
accountAddress, err := hex.DecodeString(fixture.Address)
if err != nil {
t.Fatalf("Failed to decode address: %v", err)
}

// Try to parse the account
// Note: We expect this to fail for now due to TLB parsing issues
account, err := ParseShardAccount(bocData)
account, _, err := ParseShardAccount(bocData, proofData, accountAddress)
if err != nil {
t.Logf("ParseShardAccount failed as expected (TLB issue): %v", err)
t.Logf("ParseShardAccount failed: %v", err)

// Verify we can at least deserialize the BOC structure
cells, bocErr := boc.DeserializeBoc(bocData)
Expand All @@ -477,13 +483,9 @@ func TestParseShardAccount_Integration(t *testing.T) {
}
} else {
// If parsing succeeds, validate the account
if account == nil {
t.Error("ParseShardAccount succeeded but returned nil account")
} else {
t.Logf("✅ Successfully parsed account")
t.Logf(" LastTransLt: %d", account.LastTransLt)
t.Logf(" Account type: %s", account.Account.SumType)
}
t.Logf("✅ Successfully parsed account")
t.Logf(" LastTransLt: %d", account.LastTransLt)
t.Logf(" Account type: %s", account.Account.SumType)
}
}

Expand Down
Loading