Skip to content

Commit b1baf4e

Browse files
GCdePaulaclaude
andcommitted
feat: teach the node to settle through safety-gated tasks
- epoch-manager: detect a safety gate behind the EpochSealed task via ERC-165 (pinned ISafetyGateTask id), cast a sentry vote when the signer is a sentry, start the fallback timer when sentries are missing or disagree, and point the PRT player at INNER_TASK. - state-manager: store the epoch's final machine state alongside the computation hash, since canSettle now reports the final state. - blockchain-reader: follow the EpochSealed field rename. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 0225de2 commit b1baf4e

10 files changed

Lines changed: 185 additions & 29 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.

cartesi-rollups/node/blockchain-reader/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ impl<SM: StateManager> BlockchainReader<SM> {
257257
.inputIndexUpperBound
258258
.to_u64()
259259
.expect("fail to convert epoch boundary"),
260-
root_tournament: e.tournament,
260+
root_tournament: e.task,
261261
block_created_number: meta.block_number.expect("block number should exist"),
262262
};
263263
info!(

cartesi-rollups/node/cartesi-rollups-prt-node/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ pub fn create_epoch_manager_task(watch: Watch, parameters: &PRTConfig) -> thread
9595
let epoch_manager = EpochManager::new(
9696
Arc::new(Mutex::new(arena_sender)),
9797
params.address_book.consensus,
98+
params.signer_address,
9899
state_manager,
99100
params.sleep_duration,
100101
params.long_block_range_error_codes.clone(),

cartesi-rollups/node/epoch-manager/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ repository.workspace = true
1111

1212
[dependencies]
1313
cartesi-dave-contracts = { workspace = true }
14+
cartesi-prt-contracts = { workspace = true }
1415
cartesi-prt-core = { workspace = true }
1516
rollups-state-manager = { workspace = true }
1617

cartesi-rollups/node/epoch-manager/src/lib.rs

Lines changed: 147 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
mod error;
55

66
use alloy::{
7-
primitives::{Address, B256},
7+
primitives::{Address, B256, FixedBytes},
88
providers::{DynProvider, Provider},
99
};
1010
use error::Result;
@@ -14,37 +14,49 @@ use std::{ops::ControlFlow, sync::Arc, time::Duration};
1414
use tokio::sync::Mutex;
1515

1616
use cartesi_dave_contracts::dave_consensus::DaveConsensus;
17+
use cartesi_prt_contracts::safety_gate_task;
1718
use cartesi_prt_core::{
1819
db::dispute_state_access::{Input, Leaf},
1920
strategy::player::Player,
2021
tournament::{ArenaSender, allow_revert_rethrow_others},
2122
};
22-
use rollups_state_manager::{Epoch, Proof, StateManager, sync::Watch};
23+
use rollups_state_manager::{Epoch, Proof, Settlement, StateManager, sync::Watch};
24+
25+
/// `type(ISafetyGateTask).interfaceId`, pinned by
26+
/// `testInterfaceIdMatchesNodeConstant` in `prt/contracts`.
27+
///
28+
/// Note: Solidity interface ids exclude inherited functions (`result`,
29+
/// `cleanup`, `supportsInterface`), so this cannot be derived by XORing the
30+
/// selectors of the full contract ABI.
31+
const SAFETY_GATE_TASK_INTERFACE_ID: FixedBytes<4> = FixedBytes::new([0xe1, 0x97, 0xd7, 0xb5]);
2332

2433
pub struct EpochManager<AS: ArenaSender, SM: StateManager> {
2534
arena_sender: Arc<Mutex<AS>>,
2635
consensus: Address,
36+
signer_address: Address,
2737
sleep_duration: Duration,
2838
long_block_range_error_codes: Vec<String>,
2939
state_manager: SM,
30-
last_react_epoch: (Option<Player<AS>>, u64),
40+
last_react_epoch: (Option<Player<AS>>, u64, Address),
3141
}
3242

3343
impl<AS: ArenaSender, SM: StateManager> EpochManager<AS, SM> {
3444
pub fn new(
3545
arena_sender: Arc<Mutex<AS>>,
3646
consensus_address: Address,
47+
signer_address: Address,
3748
state_manager: SM,
3849
sleep_duration: Duration,
3950
long_block_range_error_codes: Vec<String>,
4051
) -> Self {
4152
Self {
4253
arena_sender,
4354
consensus: consensus_address,
55+
signer_address,
4456
sleep_duration,
4557
long_block_range_error_codes,
4658
state_manager,
47-
last_react_epoch: (None, 0),
59+
last_react_epoch: (None, 0, Address::ZERO),
4860
}
4961
}
5062

@@ -83,9 +95,8 @@ impl<AS: ArenaSender, SM: StateManager> EpochManager<AS, SM> {
8395
)? {
8496
Some(settlement) => {
8597
assert_eq!(
86-
settlement.computation_hash.data(),
87-
can_settle.winnerCommitment,
88-
"Winner commitment mismatch, notify all users!"
98+
settlement.final_state, can_settle.finalState,
99+
"Winner state mismatch, notify all users!"
89100
);
90101
info!(
91102
"settle epoch {} with claim {}",
@@ -119,12 +130,20 @@ impl<AS: ArenaSender, SM: StateManager> EpochManager<AS, SM> {
119130
.state_manager
120131
.settlement_info(last_sealed_epoch.epoch_number)?
121132
{
122-
Some(_) => {
133+
Some(settlement) => {
123134
trace!(
124135
"dispute tournaments for epoch {}",
125136
last_sealed_epoch.epoch_number
126137
);
127-
self.react_dispute(provider, &last_sealed_epoch).await?
138+
let tournament_address = self
139+
.resolve_tournament_address(
140+
provider.clone(),
141+
last_sealed_epoch.root_tournament,
142+
&settlement,
143+
)
144+
.await?;
145+
self.react_dispute(provider, &last_sealed_epoch, tournament_address)
146+
.await?
128147
}
129148
None => {
130149
debug!(
@@ -141,8 +160,9 @@ impl<AS: ArenaSender, SM: StateManager> EpochManager<AS, SM> {
141160
&mut self,
142161
provider: DynProvider,
143162
last_sealed_epoch: &Epoch,
163+
tournament_address: Address,
144164
) -> Result<()> {
145-
self.get_latest_player(last_sealed_epoch, provider)?;
165+
self.get_latest_player(last_sealed_epoch, provider, tournament_address)?;
146166
self.last_react_epoch
147167
.0
148168
.as_mut()
@@ -157,6 +177,7 @@ impl<AS: ArenaSender, SM: StateManager> EpochManager<AS, SM> {
157177
&mut self,
158178
last_sealed_epoch: &Epoch,
159179
provider: DynProvider,
180+
tournament_address: Address,
160181
) -> Result<()> {
161182
let snapshot = self
162183
.state_manager
@@ -167,6 +188,7 @@ impl<AS: ArenaSender, SM: StateManager> EpochManager<AS, SM> {
167188
// we need to instantiate new epoch player with appropriate data
168189
if self.last_react_epoch.0.is_none()
169190
|| self.last_react_epoch.1 != last_sealed_epoch.epoch_number
191+
|| self.last_react_epoch.2 != tournament_address
170192
{
171193
let inputs = self
172194
.state_manager
@@ -191,21 +213,134 @@ impl<AS: ArenaSender, SM: StateManager> EpochManager<AS, SM> {
191213
leafs,
192214
provider.erased(),
193215
snapshot.to_string_lossy().to_string(),
194-
last_sealed_epoch.root_tournament,
216+
tournament_address,
195217
last_sealed_epoch.block_created_number,
196218
self.long_block_range_error_codes.clone(),
197219
self.state_manager
198220
.epoch_directory(last_sealed_epoch.epoch_number)?,
199221
)
200222
.expect("fail to initialize prt player");
201223

202-
self.last_react_epoch = (Some(player), last_sealed_epoch.epoch_number);
224+
self.last_react_epoch = (
225+
Some(player),
226+
last_sealed_epoch.epoch_number,
227+
tournament_address,
228+
);
229+
}
230+
231+
Ok(())
232+
}
233+
234+
/// If the epoch task is a safety gate, participate in the gate (sentry
235+
/// vote and fallback timer) and return the inner task address so the
236+
/// player can interact with the actual tournament.
237+
async fn resolve_tournament_address(
238+
&self,
239+
provider: DynProvider,
240+
task_address: Address,
241+
settlement: &Settlement,
242+
) -> Result<Address> {
243+
if let Some(inner_task) = self
244+
.try_safety_gate(provider, task_address, settlement)
245+
.await?
246+
{
247+
Ok(inner_task)
248+
} else {
249+
Ok(task_address)
250+
}
251+
}
252+
253+
async fn try_safety_gate(
254+
&self,
255+
provider: DynProvider,
256+
task_address: Address,
257+
settlement: &Settlement,
258+
) -> Result<Option<Address>> {
259+
if !supports_interface(
260+
provider.clone(),
261+
task_address,
262+
SAFETY_GATE_TASK_INTERFACE_ID,
263+
)
264+
.await
265+
{
266+
return Ok(None);
267+
}
268+
269+
let safety_gate = safety_gate_task::SafetyGateTask::new(task_address, provider.clone());
270+
271+
self.try_sentry_vote(&safety_gate, settlement).await?;
272+
try_start_fallback_timer(&safety_gate).await?;
273+
274+
let inner_task = safety_gate.INNER_TASK().call().await?;
275+
Ok(Some(inner_task))
276+
}
277+
278+
async fn try_sentry_vote(
279+
&self,
280+
safety_gate: &safety_gate_task::SafetyGateTask::SafetyGateTaskInstance<DynProvider>,
281+
settlement: &Settlement,
282+
) -> Result<()> {
283+
let is_sentry = safety_gate.isSentry(self.signer_address).call().await?;
284+
if !is_sentry {
285+
return Ok(());
286+
}
287+
288+
let has_voted = safety_gate.hasVoted(self.signer_address).call().await?;
289+
if has_voted {
290+
return Ok(());
203291
}
204292

293+
let vote = B256::from(settlement.final_state);
294+
info!(
295+
"sentry vote {} on safety gate {}",
296+
vote,
297+
safety_gate.address()
298+
);
299+
let tx_result = safety_gate.sentryVote(vote).send().await;
300+
allow_revert_rethrow_others("sentryVote", tx_result).await?;
205301
Ok(())
206302
}
207303
}
208304

305+
/// The gate never starts its own fallback timer; an offchain actor must do
306+
/// it for liveness whenever sentries are missing or disagree.
307+
async fn try_start_fallback_timer(
308+
safety_gate: &safety_gate_task::SafetyGateTask::SafetyGateTaskInstance<DynProvider>,
309+
) -> Result<()> {
310+
let can_start = safety_gate.canStartFallbackTimer().call().await?;
311+
if !can_start {
312+
return Ok(());
313+
}
314+
315+
info!(
316+
"start fallback timer on safety gate {}",
317+
safety_gate.address()
318+
);
319+
let tx_result = safety_gate.startFallbackTimer().send().await;
320+
allow_revert_rethrow_others("startFallbackTimer", tx_result).await?;
321+
Ok(())
322+
}
323+
324+
async fn supports_interface(
325+
provider: DynProvider,
326+
contract: Address,
327+
interface_id: FixedBytes<4>,
328+
) -> bool {
329+
let erc165 = safety_gate_task::SafetyGateTask::new(contract, provider);
330+
match erc165.supportsInterface(interface_id).call().await {
331+
Ok(value) => value,
332+
Err(err) => {
333+
let message = err.to_string();
334+
if message.contains("execution reverted") {
335+
trace!("supportsInterface reverted: {}", message);
336+
} else {
337+
debug!("supportsInterface call failed: {}", message);
338+
}
339+
false
340+
}
341+
}
342+
}
343+
209344
fn to_bytes_32_vec(proof: Proof) -> Vec<B256> {
210345
proof.inner().iter().map(B256::from).collect()
211346
}

cartesi-rollups/node/state-manager/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ impl Proof {
6565
#[derive(Clone, Debug, PartialEq, Eq)]
6666
pub struct Settlement {
6767
pub computation_hash: Digest,
68+
pub final_state: Hash,
6869
pub output_merkle: Hash,
6970
pub output_proof: Proof,
7071
}

cartesi-rollups/node/state-manager/src/persistent_state_access.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::{
1212

1313
use alloy::primitives::U256;
1414
use cartesi_dave_merkle::{Digest, MerkleBuilder};
15+
use cartesi_machine::types::Hash;
1516
use rusqlite::Connection;
1617

1718
#[derive(Debug)]
@@ -212,7 +213,7 @@ impl StateManager for PersistentStateAccess {
212213
let settlement = {
213214
let leafs = rollup_data::get_all_commitments(&self.connection, previous_epoch_number)?;
214215

215-
let computation_hash = if !leafs.is_empty() {
216+
let (computation_hash, final_state) = if !leafs.is_empty() {
216217
build_commitment_from_hashes(&leafs)
217218
} else {
218219
assert_eq!(machine.next_input_index_in_epoch(), 0);
@@ -226,6 +227,7 @@ impl StateManager for PersistentStateAccess {
226227

227228
Settlement {
228229
computation_hash,
230+
final_state,
229231
output_merkle,
230232
output_proof,
231233
}
@@ -286,7 +288,7 @@ impl StateManager for PersistentStateAccess {
286288
}
287289
}
288290

289-
fn build_commitment_from_hashes(state_hashes: &[CommitmentLeaf]) -> Digest {
291+
fn build_commitment_from_hashes(state_hashes: &[CommitmentLeaf]) -> (Digest, Hash) {
290292
let mut builder = MerkleBuilder::default();
291293

292294
assert!(!state_hashes.is_empty());
@@ -306,7 +308,7 @@ fn build_commitment_from_hashes(state_hashes: &[CommitmentLeaf]) -> Digest {
306308
);
307309

308310
let tree = builder.build();
309-
tree.root_hash()
311+
(tree.root_hash(), last.hash)
310312
}
311313

312314
#[cfg(test)]
@@ -526,13 +528,14 @@ mod tests {
526528
access.roll_epoch()?;
527529
assert_eq!(access.latest_snapshot()?.epoch(), 1);
528530

531+
let (computation_hash, final_state) =
532+
build_commitment_from_hashes(&[commitment_leaf_1.clone(), commitment_leaf_2.clone()]);
533+
529534
assert_eq!(
530535
access.settlement_info(0)?.unwrap(),
531536
Settlement {
532-
computation_hash: build_commitment_from_hashes(&[
533-
commitment_leaf_1.clone(),
534-
commitment_leaf_2.clone()
535-
]),
537+
computation_hash,
538+
final_state,
536539
output_merkle,
537540
output_proof
538541
},

cartesi-rollups/node/state-manager/src/sql/migrations.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
CREATE TABLE IF NOT EXISTS settlement_info (
55
epoch_number INTEGER NOT NULL PRIMARY KEY CHECK (epoch_number >= 0),
66
computation_hash BLOB NOT NULL,
7+
final_state BLOB NOT NULL,
78
output_merkle BLOB NOT NULL,
89
output_proof BLOB NOT NULL
910
);

0 commit comments

Comments
 (0)