Skip to content

Commit e660c41

Browse files
committed
fix: don't fail rollback inside the volatile ledger
Signed-off-by: Eric Torreborre <etorreborre@yahoo.com>
1 parent 7c82a0b commit e660c41

4 files changed

Lines changed: 107 additions & 12 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/amaru-ledger/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ toml.workspace = true
5050

5151
# Internal dependencies ───────────────────────────────────────────────────────┐
5252
amaru-kernel = { workspace = true, features = ["test-utils"] }
53+
amaru-stores.workspace = true
5354
amaru-tracing-json.workspace = true
5455

5556
[build-dependencies]

crates/amaru-ledger/src/state.rs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -663,18 +663,20 @@ impl<S: Store, HS: HistoricalStores> State<S, HS> {
663663
trace_span!(amaru_observability::amaru::ledger::state::ROLL_BACKWARD, rollback_point = to.to_string());
664664
let _guard = _span.enter();
665665

666-
// NOTE: This happens typically on start-up; The consensus layer will typically ask us to
667-
// rollback to the last known point, which ought to be the tip of the database.
668-
if self.volatile.is_empty() && self.tip().as_ref() == to {
669-
return Ok(());
670-
}
671-
672-
if self.tip().as_ref() > to {
673-
return Err(BackwardError::RollbackPointBeforeTip { rollback_point: *to, tip: self.tip().into_owned() });
674-
}
675-
676-
if self.volatile.is_empty() && self.tip().as_ref() < to {
677-
return Err(BackwardError::RollbackPointInFuture(*to));
666+
if self.volatile.is_empty() {
667+
// NOTE: This happens typically on start-up; The consensus layer will typically ask us to
668+
// rollback to the last known point, which ought to be the tip of the database.
669+
// Otherwise we return an error.
670+
if self.tip().as_ref() == to {
671+
return Ok(());
672+
} else if self.tip().as_ref() > to {
673+
return Err(BackwardError::RollbackPointBeforeTip {
674+
rollback_point: *to,
675+
tip: self.tip().into_owned(),
676+
});
677+
} else {
678+
return Err(BackwardError::RollbackPointInFuture(*to));
679+
}
678680
}
679681

680682
if let Some(last) = self.volatile.view_back()
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright 2026 PRAGMA
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use std::collections::VecDeque;
16+
17+
use amaru_kernel::{
18+
BlockHeight, EraHistory, GlobalParameters, Hash, NetworkName, Point, ProtocolParameters, Slot, Tip,
19+
};
20+
use amaru_ledger::{
21+
state::{BackwardError, State, VolatileState},
22+
store::GovernanceActivity,
23+
};
24+
use amaru_stores::in_memory::MemoryStore;
25+
26+
#[test]
27+
fn rollback_to_a_volatile_common_ancestor_succeeds() {
28+
let mut state = make_state();
29+
let earlier = point(100, 1);
30+
let later = point(200, 2);
31+
32+
forward_to(&mut state, earlier, 1);
33+
forward_to(&mut state, later, 2);
34+
assert_eq!(*state.tip(), later);
35+
36+
state.rollback_to(&earlier).unwrap();
37+
assert_eq!(*state.tip(), earlier);
38+
}
39+
40+
#[test]
41+
fn rollback_before_volatile_front_is_rejected() {
42+
let mut state = make_state();
43+
forward_to(&mut state, point(100, 1), 1);
44+
forward_to(&mut state, point(200, 2), 2);
45+
46+
let before_volatile = point(50, 9);
47+
let err = state.rollback_to(&before_volatile).err().unwrap();
48+
49+
assert!(
50+
matches!(err, BackwardError::UnknownRollbackPoint(p) if p == before_volatile),
51+
"expected UnknownRollbackPoint, got {err:?}",
52+
);
53+
assert_eq!(*state.tip(), point(200, 2), "tip is unchanged after a rejected rollback");
54+
}
55+
56+
// HELPERS
57+
58+
/// Create an initial ledger state
59+
fn make_state() -> State<MemoryStore, MemoryStore> {
60+
let network = NetworkName::Preprod;
61+
let era_history: EraHistory = <&EraHistory>::from(network).clone();
62+
let global_parameters: GlobalParameters = <&GlobalParameters>::from(network).clone();
63+
let protocol_parameters: ProtocolParameters =
64+
<&ProtocolParameters>::try_from(network).expect("preprod parameters available").clone();
65+
66+
let store = MemoryStore::new(era_history.clone(), protocol_parameters.clone());
67+
let snapshots = MemoryStore::new(era_history.clone(), protocol_parameters.clone());
68+
69+
State::new_with(
70+
store,
71+
snapshots,
72+
network,
73+
era_history,
74+
global_parameters,
75+
protocol_parameters,
76+
GovernanceActivity { consecutive_dormant_epochs: 0 },
77+
VecDeque::new(),
78+
)
79+
}
80+
81+
/// Forward the ldeger to a given point
82+
fn forward_to(state: &mut State<MemoryStore, MemoryStore>, point: Point, height: u64) {
83+
let issuer = Hash::new([0u8; 28]);
84+
state
85+
.forward(VolatileState::default().anchor(Tip::new(point, BlockHeight::from(height)), issuer))
86+
.expect("forward");
87+
}
88+
89+
fn point(slot: u64, tag: u8) -> Point {
90+
Point::Specific(Slot::from(slot), Hash::new([tag; 32]))
91+
}

0 commit comments

Comments
 (0)