Skip to content
This repository was archived by the owner on Mar 23, 2026. It is now read-only.
Open
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
64 changes: 62 additions & 2 deletions src/chain/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub use self::error::{StateError, StateErrorKind};

use crate::{
error::{Error, ErrorKind::*},
prelude::*,
prelude::*, privval::SignableMsg,
};
use std::{
fs,
Expand All @@ -19,11 +19,15 @@ use std::{
};
use tempfile::NamedTempFile;
use tendermint::consensus;
use std::collections::BTreeMap;
use tendermint::block::{Height, Round};

/// State tracking for double signing prevention
pub struct State {
consensus_state: consensus::State,
state_file_path: PathBuf,

last_signs: BTreeMap<(Height,Round,i8), SignableMsg>,
}

impl State {
Expand All @@ -46,6 +50,7 @@ impl State {
Ok(Self {
consensus_state,
state_file_path: path.as_ref().to_owned(),
last_signs: BTreeMap::new(),
})
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {
Expand Down Expand Up @@ -172,6 +177,7 @@ impl State {
let initial_state = Self {
consensus_state,
state_file_path: path.to_owned(),
last_signs: BTreeMap::new(),
};

initial_state.sync_to_disk()?;
Expand Down Expand Up @@ -204,12 +210,22 @@ impl State {

Ok(())
}

pub fn get_last_vote(&self, height: Height, round: Round, step: i8) -> Option<SignableMsg> {
self.last_signs.get(&(height, round, step)).cloned()
}

pub fn save_sign(&mut self, height: Height, round: Round, step: i8, signable_msg: SignableMsg) {
self.last_signs.insert((height, round, step), signable_msg);
self.last_signs.retain(|key, _| height.value() < 100 + key.0.value());
}
}

#[cfg(test)]
mod tests {
use super::*;
use tendermint::block;
use crate::privval::SignableMsg;
use tendermint::{block, proposal::{Proposal, Type}};

const EXAMPLE_BLOCK_ID: &str =
"26C0A41F3243C6BCD7AD2DFF8A8D83A71D29D307B5326C227F734A1A512FE47D";
Expand Down Expand Up @@ -246,6 +262,7 @@ mod tests {
State {
consensus_state: $old_state,
state_file_path: EXAMPLE_PATH.into(),
last_signs: BTreeMap::new(),
}
.update_consensus_state($new_state)
.unwrap();
Expand All @@ -261,6 +278,7 @@ mod tests {
let err = State {
consensus_state: $old_state,
state_file_path: EXAMPLE_PATH.into(),
last_signs: BTreeMap::new(),
}
.update_consensus_state($new_state)
.expect_err("expected StateErrorKind::DoubleSign but succeeded");
Expand Down Expand Up @@ -311,4 +329,46 @@ mod tests {
state!(1, 1, 2, None),
state!(1, 1, 2, block_id!(EXAMPLE_BLOCK_ID))
);

#[test]
fn test_save_sign() {
let mut state = State {
consensus_state: state!(1, 1, 0, None),
state_file_path: EXAMPLE_PATH.into(),
last_signs: BTreeMap::new(),
};

state.save_sign(Height::from(1 as u32), Round::from(1 as u16), 0, SignableMsg::Proposal(Proposal{
msg_type: Type::Proposal,
pol_round: None,
block_id: None,
timestamp: None,
signature: None,
height: Height::from(1 as u32),
round: Round::from(1 as u16),
}));

assert_eq!(state.last_signs.len(), 1);
state.save_sign(Height::from(100 as u32), Round::from(1 as u16), 0, SignableMsg::Proposal(Proposal{
msg_type: Type::Proposal,
pol_round: None,
block_id: None,
timestamp: None,
signature: None,
height: Height::from(100 as u32),
round: Round::from(1 as u16),
}));
assert_eq!(state.last_signs.len(), 2);

state.save_sign(Height::from(101 as u32), Round::from(1 as u16), 0, SignableMsg::Proposal(Proposal{
msg_type: Type::Proposal,
pol_round: None,
block_id: None,
timestamp: None,
signature: None,
height: Height::from(101 as u32),
round: Round::from(1 as u16),
}));
assert_eq!(state.last_signs.len(), 2);
}
Comment on lines +333 to +373
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix code formatting issues.

The test code demonstrates the pruning functionality well, but there are formatting issues that need to be addressed.

Based on the pipeline failure, please run cargo fmt to fix the formatting issues:

cargo fmt

The test logic itself is sound - it verifies that:

  1. Signatures are added to the cache
  2. Pruning occurs when height advances beyond the 100-height window
🤖 Prompt for AI Agents
In src/chain/state.rs around lines 333 to 373, the test_save_sign function has
formatting issues causing pipeline failures. Run the Rust formatter by executing
`cargo fmt` in the project root to automatically fix indentation, spacing, and
line breaks according to Rust style guidelines. This will resolve the formatting
problems without altering the test logic.

}
2 changes: 1 addition & 1 deletion src/privval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const PRECOMMIT_CODE: SignedMsgCode = 0x02;
const PROPOSAL_CODE: SignedMsgCode = 0x20;

/// Trait for signed messages.
#[derive(Debug)]
#[derive(Debug,Clone)]
pub enum SignableMsg {
/// Proposals
Proposal(Proposal),
Expand Down
30 changes: 23 additions & 7 deletions src/session.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
//! A session with a validator node

use crate::{
chain::{self, state::StateErrorKind, Chain},
config::ValidatorConfig,
connection::{tcp, unix::UnixConnection, Connection},
error::{Error, ErrorKind::*},
prelude::*,
privval::SignableMsg,
rpc::{Request, Response},
chain::{self, state::StateErrorKind, Chain}, config::ValidatorConfig, connection::{tcp, unix::UnixConnection, Connection}, error::{Error, ErrorKind::*}, keyring::Signature, prelude::*, privval::SignableMsg, rpc::{Request, Response}
};
use std::{os::unix::net::UnixStream, time::Instant};
use tendermint::{consensus, TendermintKey};
Expand Down Expand Up @@ -135,7 +129,15 @@ impl Session {
});

if let Some(remote_err) = self.update_consensus_state(chain, &signable_msg)? {
// regression
if remote_err.code == 3 {
if let Some(last_sign) = self.check_existing_sign(chain, &signable_msg) {
return Ok(last_sign.into());
}
}

// In the event of double signing we send a response to notify the validator

return Ok(Response::error(signable_msg, remote_err));
}

Expand Down Expand Up @@ -165,6 +167,7 @@ impl Session {
}
}

self.save_sign(chain, signable_msg.clone());
Ok(signable_msg.into())
}

Expand Down Expand Up @@ -229,6 +232,19 @@ impl Session {
}
}

fn check_existing_sign(&self, chain: &Chain, signable_msg: &SignableMsg) -> Option<SignableMsg> {
let request_state = signable_msg.consensus_state();
let chain_state = chain.state.lock().unwrap();

chain_state.get_last_vote(request_state.height, request_state.round, request_state.step)
}

fn save_sign(&self, chain: &Chain, signable_msg: SignableMsg) {
let request_state = signable_msg.consensus_state();
let mut chain_state = chain.state.lock().unwrap();
chain_state.save_sign(request_state.height, request_state.round, request_state.step, signable_msg);
}

/// Get the public key for (the only) public key in the keyring
fn get_public_key(&mut self) -> Result<Response, Error> {
let registry = chain::REGISTRY.get();
Expand Down
Loading