Skip to content

Commit 53fa2d1

Browse files
Eagerly transition to SAE for state sync (#5916)
Signed-off-by: Rahul Muttineni <rahul.muttineni@avalabs.org> Co-authored-by: Austin Larson <78000745+alarso16@users.noreply.github.com>
1 parent b252c48 commit 53fa2d1

7 files changed

Lines changed: 277 additions & 39 deletions

File tree

node/node.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,6 +1246,7 @@ func (n *Node) initVMs() error {
12461246
// is around a second, so 10 seconds provides plenty of time to
12471247
// ensure this doesn't happen.
12481248
TransitionTime: n.Config.UpgradeConfig.HeliconTime.Add(-10 * time.Second),
1249+
Now: time.Now,
12491250
APIDrainTimeout: 15 * time.Second,
12501251
}),
12511252
)

vms/saevm/statesync/syncer.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ func (s *Syncer) ShouldAcceptSummary(summary *Summary) bool {
7575
}
7676

7777
// If any blocks have been accepted, don't state sync.
78+
//
79+
// TransitionVM assumes that a node will state-sync if state-sync is enabled
80+
// and the node is at the genesis block. Until transitionvm is removed, this
81+
// check MUST NOT change.
7882
hash := rawdb.ReadHeadFastBlockHash(s.db)
7983
if hash == (common.Hash{}) {
8084
s.snowCtx.Log.Warn("no last accepted hash")

vms/transitionvm/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ go_library(
2626
"//snow/engine/snowman/block",
2727
"//snow/validators",
2828
"//utils",
29+
"//utils/constants",
2930
"//utils/logging",
3031
"//utils/set",
3132
"//version",
@@ -57,6 +58,7 @@ go_test(
5758
"//snow/engine/snowman/block",
5859
"//snow/engine/snowman/block/blocktest",
5960
"//snow/snowtest",
61+
"//utils/constants",
6062
"//utils/logging",
6163
"//utils/set",
6264
"//version",

vms/transitionvm/README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ n.VMManager.RegisterFactory(context.TODO(), constants.EVMID, &transitionvm.Facto
2929
PreFactory: &coreth.Factory{},
3030
PostFactory: &saevm.Factory{},
3131
TransitionTime: n.Config.UpgradeConfig.HeliconTime.Add(-10 * time.Second),
32+
Now: time.Now,
3233
APIDrainTimeout: 15 * time.Second,
3334
})
3435
```
@@ -84,6 +85,50 @@ flowchart TD
8485
class E errCls;
8586
```
8687

88+
### Eager transition for state sync
89+
90+
The engine runs state sync once, at startup, against the active VM — but a
91+
node starting after the transition faces peers that serve only the
92+
post-transition VM's summaries. So a node transitions *eagerly* during
93+
initialization when the wall clock is past the transition time, the network is
94+
a production network (Mainnet or Fuji), the chain is still at the genesis, and
95+
the node intends to state sync. The marker is written before the sync runs, so
96+
the commitment is **one-way**.
97+
98+
Only production networks get the eager path. The real requirement is that the
99+
network sequenced at least one commit interval of blocks after the
100+
transition, so peers have a post-transition summary to serve; production
101+
networks are known to satisfy it, while a custom network may transition right
102+
at its genesis and strand an eagerly-committed node.
103+
104+
The eager path requires of the VMs:
105+
106+
- The **pre-transition VM reports during initialization whether the node will
107+
state sync**.
108+
- The **post-transition VM state syncs a fresh database** whenever it can sync
109+
at all: a node at the genesis cannot execute a chain with a synchronous
110+
prefix. A node with accepted blocks is refused instead — it never
111+
transitions eagerly, and bootstrap-executes to the transition block. A
112+
summary at a pre-transition height is rejected once its fetched header
113+
proves it synchronous, before any state is written.
114+
115+
A committed node never falls back to executing pre-transition blocks, so it
116+
strands until a summary it accepts arrives — or forever:
117+
118+
- **A wall-clock false positive** — a skewed clock, or a network that has not
119+
yet built the transition block — strands the node until the network
120+
transitions.
121+
- **No post-transition commit boundary yet**: nothing to sync to; resolves
122+
itself.
123+
- **State sync disabled after the marker is written**: the engine skips state
124+
sync entirely and falls into a bootstrap it cannot execute.
125+
- **State schemes that cannot sync** (Firewood): every summary is declined,
126+
forever.
127+
128+
To recover, delete the chain database and restart. For never-syncable
129+
configurations, also disable state sync in the new configuration; on the
130+
already-marked node the flag no longer helps.
131+
87132
### Swapping the VM underneath the node
88133

89134
The consensus engine, network, and API server treat a chain's VM as one

vms/transitionvm/factory.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type Factory struct {
2020
PreFactory vms.Factory
2121
PostFactory vms.Factory
2222
TransitionTime time.Time
23+
Now func() time.Time
2324
// APIDrainTimeout bounds how long the transition waits for in-flight API
2425
// requests to the pre-transition chain to return before shutting it down.
2526
APIDrainTimeout time.Duration
@@ -50,6 +51,7 @@ func (f *Factory) New(log logging.Logger) (interface{}, error) {
5051
preTransitionChain: pre,
5152
postTransitionChain: post,
5253
transitionTime: f.TransitionTime,
54+
now: f.Now,
5355
apiDrainTimeout: f.APIDrainTimeout,
5456

5557
// [VM.Version] and [VM.Shutdown] may be called before [VM.Initialize],

vms/transitionvm/vm.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/ava-labs/avalanchego/snow/consensus/snowman"
2222
"github.com/ava-labs/avalanchego/snow/engine/common"
2323
"github.com/ava-labs/avalanchego/utils"
24+
"github.com/ava-labs/avalanchego/utils/constants"
2425

2526
smblock "github.com/ava-labs/avalanchego/snow/engine/snowman/block"
2627
)
@@ -44,6 +45,7 @@ type VM struct {
4445
preTransitionChain Chain
4546
postTransitionChain Chain
4647
transitionTime time.Time
48+
now func() time.Time
4749
apiDrainTimeout time.Duration
4850

4951
// chain parameters
@@ -142,7 +144,33 @@ func (vm *VM) Initialize(
142144
return fmt.Errorf("loading last accepted block %s: %w", lastAcceptedID, err)
143145
}
144146
if lastAccepted.Timestamp().Before(vm.transitionTime) {
145-
return nil
147+
if vm.now().Before(vm.transitionTime) {
148+
return nil
149+
}
150+
// Transitioning is only safe once the network has sequenced at least
151+
// one commit interval of blocks after the transition, so peers have a
152+
// post-transition summary to serve. The production networks are known
153+
// to satisfy this; a custom network may not, so it waits for the
154+
// transition block instead.
155+
if !constants.ProductionNetworkIDs.Contains(preChainCtx.NetworkID) {
156+
log.Info("past transition time on a non-production network; waiting for the transition block")
157+
return nil
158+
}
159+
// The network is past the transition time, so peers only serve the
160+
// post-transition chain's state summaries.
161+
if lastAccepted.Height() > 0 {
162+
log.Info("past transition time with accepted pre-transition blocks; waiting for the transition block")
163+
return nil
164+
}
165+
enabled, err := vm.StateSyncEnabled(ctx)
166+
if err != nil {
167+
return fmt.Errorf("checking whether the node will state sync: %w", err)
168+
}
169+
if !enabled {
170+
log.Info("past transition time but not state syncing; waiting for the transition block")
171+
return nil
172+
}
173+
log.Info("transitioning eagerly to state sync as the post-transition chain")
146174
}
147175
return vm.transition(ctx, lastAccepted)
148176
}

0 commit comments

Comments
 (0)