Skip to content

Commit 0d342da

Browse files
authored
Simplex Block Builder Component (#4159)
1 parent 20484fc commit 0d342da

File tree

9 files changed

+361
-34
lines changed

9 files changed

+361
-34
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ require (
8888
github.com/Microsoft/go-winio v0.6.1 // indirect
8989
github.com/VictoriaMetrics/fastcache v1.12.1 // indirect
9090
github.com/ava-labs/firewood-go-ethhash/ffi v0.0.12 // indirect
91-
github.com/ava-labs/simplex v0.0.0-20250819180907-c9b5ae6aad19
91+
github.com/ava-labs/simplex v0.0.0-20250919142550-9cdfff10fd19
9292
github.com/beorn7/perks v1.0.1 // indirect
9393
github.com/bits-and-blooms/bitset v1.10.0 // indirect
9494
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ github.com/ava-labs/ledger-avalanche/go v0.0.0-20241009183145-e6f90a8a1a60 h1:EL
7878
github.com/ava-labs/ledger-avalanche/go v0.0.0-20241009183145-e6f90a8a1a60/go.mod h1:/7qKobTfbzBu7eSTVaXMTr56yTYk4j2Px6/8G+idxHo=
7979
github.com/ava-labs/libevm v1.13.14-0.3.0.rc.6 h1:tyM659nDOknwTeU4A0fUVsGNIU7k0v738wYN92nqs/Y=
8080
github.com/ava-labs/libevm v1.13.14-0.3.0.rc.6/go.mod h1:zP/DOcABRWargBmUWv1jXplyWNcfmBy9cxr0lw3LW3g=
81-
github.com/ava-labs/simplex v0.0.0-20250819180907-c9b5ae6aad19 h1:dtsx6DJw6wWkrMwDIcSUVJ9lXMeckgI5oZ7fJbAi23c=
82-
github.com/ava-labs/simplex v0.0.0-20250819180907-c9b5ae6aad19/go.mod h1:GVzumIo3zR23/qGRN2AdnVkIPHcKMq/D89EGWZfMGQ0=
81+
github.com/ava-labs/simplex v0.0.0-20250919142550-9cdfff10fd19 h1:S6oFasZsplNmw8B2S8cMJQMa62nT5ZKGzZRdCpd+5qQ=
82+
github.com/ava-labs/simplex v0.0.0-20250919142550-9cdfff10fd19/go.mod h1:GVzumIo3zR23/qGRN2AdnVkIPHcKMq/D89EGWZfMGQ0=
8383
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
8484
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
8585
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=

simplex/block.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,20 @@ type Block struct {
4141
blockTracker *blockTracker
4242
}
4343

44+
func newBlock(metadata simplex.ProtocolMetadata, vmBlock snowman.Block, blockTracker *blockTracker) (*Block, error) {
45+
block := &Block{
46+
metadata: metadata,
47+
vmBlock: vmBlock,
48+
blockTracker: blockTracker,
49+
}
50+
bytes, err := block.Bytes()
51+
if err != nil {
52+
return nil, fmt.Errorf("failed to serialize block: %w", err)
53+
}
54+
block.digest = computeDigest(bytes)
55+
return block, nil
56+
}
57+
4458
// CanotoSimplexBlock is the Canoto representation of a block
4559
type canotoSimplexBlock struct {
4660
Metadata []byte `canoto:"bytes,1"`

simplex/block_builder.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package simplex
5+
6+
import (
7+
"context"
8+
"time"
9+
10+
"github.com/ava-labs/simplex"
11+
"go.uber.org/zap"
12+
13+
"github.com/ava-labs/avalanchego/snow/engine/common"
14+
"github.com/ava-labs/avalanchego/snow/engine/snowman/block"
15+
"github.com/ava-labs/avalanchego/utils/logging"
16+
)
17+
18+
var _ simplex.BlockBuilder = (*BlockBuilder)(nil)
19+
20+
type BlockBuilder struct {
21+
log logging.Logger
22+
vm block.ChainVM
23+
blockTracker *blockTracker
24+
}
25+
26+
const (
27+
maxBackoff = 5 * time.Second
28+
initBackoff = 10 * time.Millisecond
29+
)
30+
31+
// BuildBlock continuously tries to build a block until the context is cancelled. If there are no blocks to be built, it will wait for an event from the VM.
32+
// It returns false if the context was cancelled, otherwise it returns the built block and true.
33+
func (b *BlockBuilder) BuildBlock(ctx context.Context, metadata simplex.ProtocolMetadata) (simplex.VerifiedBlock, bool) {
34+
for curWait := initBackoff; ; curWait = backoff(ctx, curWait) {
35+
if ctx.Err() != nil {
36+
b.log.Debug("Context cancelled, stopping block building", zap.Error(ctx.Err()))
37+
return nil, false
38+
}
39+
40+
err := b.waitForPendingBlock(ctx)
41+
if err != nil {
42+
b.log.Debug("Error waiting for incoming block", zap.Error(err))
43+
continue
44+
}
45+
vmBlock, err := b.vm.BuildBlock(ctx)
46+
if err != nil {
47+
b.log.Info("Error building block", zap.Error(err))
48+
continue
49+
}
50+
simplexBlock, err := newBlock(metadata, vmBlock, b.blockTracker)
51+
if err != nil {
52+
b.log.Error("Error creating simplex block from built block", zap.Error(err))
53+
return nil, false
54+
}
55+
curWait = initBackoff // Reset backoff after a successful block build
56+
verifiedBlock, err := simplexBlock.Verify(ctx)
57+
if err != nil {
58+
b.log.Warn("Error verifying block we built ourselves", zap.Error(err))
59+
continue
60+
}
61+
62+
return verifiedBlock, true
63+
}
64+
}
65+
66+
// WaitForPendingBlock blocks until a new block is ready to be built from the VM, or until the
67+
// context is cancelled.
68+
func (b *BlockBuilder) WaitForPendingBlock(ctx context.Context) {
69+
err := b.waitForPendingBlock(ctx)
70+
if err != nil {
71+
b.log.Debug("Error waiting for incoming block", zap.Error(err))
72+
}
73+
}
74+
75+
func (b *BlockBuilder) waitForPendingBlock(ctx context.Context) error {
76+
for curBackoff := initBackoff; ; curBackoff = backoff(ctx, curBackoff) {
77+
msg, err := b.vm.WaitForEvent(ctx)
78+
if err != nil {
79+
return err
80+
}
81+
if msg == common.PendingTxs {
82+
return nil
83+
}
84+
b.log.Warn("Received unexpected message", zap.Stringer("message", msg))
85+
}
86+
}
87+
88+
// backoff waits for `backoff` duration before returning the next backoff duration.
89+
// It doubles the backoff duration each time it is called, up to a maximum of `maxBackoff`.
90+
func backoff(ctx context.Context, backoff time.Duration) time.Duration {
91+
select {
92+
case <-ctx.Done():
93+
return 0
94+
case <-time.After(backoff):
95+
}
96+
97+
return min(maxBackoff, 2*backoff) // exponential backoff
98+
}

simplex/block_builder_test.go

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package simplex
5+
6+
import (
7+
"context"
8+
"errors"
9+
"testing"
10+
"time"
11+
12+
"github.com/ava-labs/simplex"
13+
"github.com/stretchr/testify/require"
14+
15+
"github.com/ava-labs/avalanchego/snow/consensus/snowman"
16+
"github.com/ava-labs/avalanchego/snow/engine/common"
17+
"github.com/ava-labs/avalanchego/utils/logging"
18+
)
19+
20+
func TestBlockBuilder(t *testing.T) {
21+
ctx := context.Background()
22+
genesis := newTestBlock(t, newBlockConfig{})
23+
child := newTestBlock(t, newBlockConfig{
24+
prev: genesis,
25+
})
26+
27+
tests := []struct {
28+
name string
29+
block snowman.Block
30+
shouldBuild bool
31+
expectedBlock simplex.VerifiedBlock
32+
vmBlockBuildF func(ctx context.Context) (snowman.Block, error)
33+
}{
34+
{
35+
name: "build block successfully",
36+
block: child.vmBlock,
37+
shouldBuild: true,
38+
expectedBlock: child,
39+
vmBlockBuildF: func(_ context.Context) (snowman.Block, error) {
40+
return child.vmBlock, nil
41+
},
42+
},
43+
{
44+
name: "fail to build block",
45+
block: nil,
46+
shouldBuild: false,
47+
vmBlockBuildF: func(_ context.Context) (snowman.Block, error) {
48+
return nil, errors.New("failed to build block")
49+
},
50+
},
51+
{
52+
name: "fail to verify block",
53+
block: nil,
54+
vmBlockBuildF: func(_ context.Context) (snowman.Block, error) {
55+
b := newTestBlock(t, newBlockConfig{
56+
prev: genesis,
57+
})
58+
b.vmBlock.(*wrappedBlock).VerifyV = errors.New("verification failed")
59+
return b.vmBlock, nil
60+
},
61+
shouldBuild: false,
62+
},
63+
}
64+
65+
for _, tt := range tests {
66+
t.Run(tt.name, func(t *testing.T) {
67+
count := 0
68+
vm := newTestVM()
69+
70+
vm.WaitForEventF = func(_ context.Context) (common.Message, error) {
71+
count++
72+
return common.PendingTxs, nil
73+
}
74+
vm.BuildBlockF = tt.vmBlockBuildF
75+
76+
bb := &BlockBuilder{
77+
log: logging.NoLog{},
78+
vm: vm,
79+
blockTracker: genesis.blockTracker,
80+
}
81+
82+
timeoutCtx, cancelCtx := context.WithTimeout(ctx, 100*time.Millisecond)
83+
defer cancelCtx()
84+
85+
block, built := bb.BuildBlock(timeoutCtx, child.BlockHeader().ProtocolMetadata)
86+
require.Equal(t, tt.shouldBuild, built)
87+
require.Equal(t, tt.expectedBlock, block)
88+
if tt.expectedBlock == nil {
89+
require.Greater(t, count, 1)
90+
}
91+
})
92+
}
93+
}
94+
95+
func TestBlockBuilderCancelContext(t *testing.T) {
96+
ctx := context.Background()
97+
vm := newTestVM()
98+
genesis := newTestBlock(t, newBlockConfig{})
99+
child := newTestBlock(t, newBlockConfig{
100+
prev: genesis,
101+
})
102+
vm.WaitForEventF = func(ctx context.Context) (common.Message, error) {
103+
<-ctx.Done()
104+
return 0, ctx.Err()
105+
}
106+
107+
bb := &BlockBuilder{
108+
log: logging.NoLog{},
109+
vm: vm,
110+
blockTracker: genesis.blockTracker,
111+
}
112+
113+
timeoutCtx, cancelCtx := context.WithTimeout(ctx, 100*time.Millisecond)
114+
defer cancelCtx()
115+
116+
_, built := bb.BuildBlock(timeoutCtx, child.BlockHeader().ProtocolMetadata)
117+
require.False(t, built, "Block should not be built when context is cancelled")
118+
}
119+
120+
func TestWaitForPendingBlock(t *testing.T) {
121+
ctx := context.Background()
122+
vm := newTestVM()
123+
genesis := newTestBlock(t, newBlockConfig{})
124+
count := 0
125+
vm.WaitForEventF = func(_ context.Context) (common.Message, error) {
126+
if count == 0 {
127+
count++
128+
return common.StateSyncDone, nil
129+
}
130+
return common.PendingTxs, nil
131+
}
132+
133+
bb := &BlockBuilder{
134+
log: logging.NoLog{},
135+
vm: vm,
136+
blockTracker: genesis.blockTracker,
137+
}
138+
139+
bb.WaitForPendingBlock(ctx)
140+
require.Equal(t, 1, count)
141+
}
142+
143+
func TestBlockBuildingExponentialBackoff(t *testing.T) {
144+
ctx := context.Background()
145+
vm := newTestVM()
146+
genesis := newTestBlock(t, newBlockConfig{})
147+
child := newTestBlock(t, newBlockConfig{
148+
prev: genesis,
149+
})
150+
const (
151+
failedAttempts = 7
152+
minimumExpectedDelay = initBackoff * (1<<failedAttempts - 1)
153+
)
154+
155+
vm.WaitForEventF = func(_ context.Context) (common.Message, error) {
156+
return common.PendingTxs, nil
157+
}
158+
159+
count := 0
160+
vm.BuildBlockF = func(_ context.Context) (snowman.Block, error) {
161+
if count < failedAttempts {
162+
count++
163+
return nil, errors.New("failed to build block")
164+
}
165+
166+
// on the 8th try, return the block successfully
167+
return child.vmBlock, nil
168+
}
169+
170+
bb := &BlockBuilder{
171+
log: logging.NoLog{},
172+
vm: vm,
173+
blockTracker: genesis.blockTracker,
174+
}
175+
176+
start := time.Now()
177+
block, built := bb.BuildBlock(ctx, child.BlockHeader().ProtocolMetadata)
178+
endTime := time.Since(start)
179+
180+
require.True(t, built)
181+
require.Equal(t, child.BlockHeader(), block.BlockHeader())
182+
183+
// 10, 20, 40, 80, 160, 320, 640 = 1270ms
184+
require.GreaterOrEqual(t, endTime, minimumExpectedDelay)
185+
}
186+
187+
func TestWaitForPendingBlockBackoff(t *testing.T) {
188+
ctx := context.Background()
189+
vm := newTestVM()
190+
const (
191+
failedAttempts = 7
192+
minimumExpectedDelay = initBackoff * (1<<failedAttempts - 1)
193+
)
194+
195+
count := 0
196+
vm.WaitForEventF = func(_ context.Context) (common.Message, error) {
197+
if count < failedAttempts {
198+
count++
199+
return common.StateSyncDone, nil
200+
}
201+
202+
return common.PendingTxs, nil
203+
}
204+
205+
bb := &BlockBuilder{
206+
log: logging.NoLog{},
207+
vm: vm,
208+
blockTracker: nil,
209+
}
210+
211+
start := time.Now()
212+
bb.WaitForPendingBlock(ctx)
213+
endTime := time.Since(start)
214+
215+
// 10, 20, 40, 80, 160, 320, 640 = 1270ms
216+
require.GreaterOrEqual(t, endTime, minimumExpectedDelay)
217+
}

0 commit comments

Comments
 (0)