Skip to content
This repository has been archived by the owner on Jun 3, 2020. It is now read-only.

Commit

Permalink
Merge pull request #193 from tendermint/zaki/priv_validator_state
Browse files Browse the repository at this point in the history
State tracking for double sign protection
  • Loading branch information
tarcieri committed Mar 8, 2019
2 parents 3bfe6f1 + 9917287 commit 52b7295
Show file tree
Hide file tree
Showing 10 changed files with 297 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
command: |
rustc --version
cargo --version
cargo test --all --all-features
cargo test --all --all-features -- --test-threads 1
- run:
name: audit
command: |
Expand Down
11 changes: 11 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Error types

use crate::last_sign_state;
use crate::prost;
use abscissa::Error;
use signatory;
Expand Down Expand Up @@ -77,6 +78,10 @@ pub enum KmsErrorKind {
/// Verification operation failed
#[fail(display = "verification failed")]
VerificationError,

/// Signature invalid
#[fail(display = "attempted double sign")]
DoubleSign,
}

impl Display for KmsError {
Expand Down Expand Up @@ -145,3 +150,9 @@ impl From<TmValidationError> for KmsError {
err!(KmsErrorKind::InvalidMessageError, other).into()
}
}

impl From<last_sign_state::LastSignError> for KmsError {
fn from(other: last_sign_state::LastSignError) -> Self {
err!(KmsErrorKind::DoubleSign, other).into()
}
}
200 changes: 200 additions & 0 deletions src/last_sign_state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
use abscissa::Error;
use serde_json;
use std::{
fmt::{self, Display},
fs::{File, OpenOptions},
io::prelude::*,
path::Path,
};
use tendermint::{block, chain};

#[derive(Serialize, Deserialize)]
struct LastSignData {
pub height: i64,
pub round: i64,
pub step: i8,
pub block_id: Option<block::Id>,
}

const EMPTY_DATA: LastSignData = LastSignData {
height: 0,
round: 0,
step: 0,
block_id: None,
};

pub struct LastSignState {
data: LastSignData,
file: File,
_chain_id: chain::Id,
}

/// Error type
#[derive(Debug)]
pub struct LastSignError(Error<LastSignErrorKind>);

#[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)]
pub enum LastSignErrorKind {
#[fail(display = "height regression")]
HeightRegression,
#[fail(display = "step regression")]
StepRegression,
#[fail(display = "round regression")]
RoundRegression,
#[fail(display = "invalid block id")]
DoubleSign,
}

impl From<Error<LastSignErrorKind>> for LastSignError {
fn from(other: Error<LastSignErrorKind>) -> Self {
LastSignError(other)
}
}

impl Display for LastSignError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}

impl LastSignState {
pub fn load_state(path: &Path, chain_id: chain::Id) -> std::io::Result<LastSignState> {
if !path.exists() {
let mut lst = LastSignState {
data: EMPTY_DATA,
file: File::create(path)?,
_chain_id: chain_id,
};
lst.sync_to_disk()?;
return Ok(lst);
}
let mut lst = LastSignState {
data: EMPTY_DATA,
file: OpenOptions::new().read(true).write(true).open(path)?,
_chain_id: chain_id,
};

let mut contents = String::new();
lst.file.read_to_string(&mut contents)?;
lst.data = serde_json::from_str(&contents)?;
Ok(lst)
}

pub fn sync_to_disk(&mut self) -> std::io::Result<()> {
self.file
.write_all(serde_json::to_string(&self.data)?.as_ref())?;
self.file.sync_all()?;
Ok(())
}

pub fn check_and_update_hrs(
&mut self,
height: i64,
round: i64,
step: i8,
block_id: Option<block::Id>,
) -> Result<(), LastSignError> {
if height < self.data.height {
fail!(
LastSignErrorKind::HeightRegression,
"last height:{} new height:{}",
self.data.height,
height
);
}
if height == self.data.height {
if round < self.data.round {
fail!(
LastSignErrorKind::RoundRegression,
"round regression at height:{} last round:{} new round:{}",
height,
self.data.round,
round
)
}
if round == self.data.round {
if step < self.data.step {
fail!(
LastSignErrorKind::StepRegression,
"round regression at height:{} round:{} last step:{} new step:{}",
height,
round,
self.data.step,
step
)
}

if block_id != None && self.data.block_id != None && self.data.block_id != block_id
{
fail!(
LastSignErrorKind::DoubleSign,
"Attempting to sign a second proposal at height:{} round:{} step:{} old block id:{} new block {}",
height,
round,
step,
self.data.block_id.clone().unwrap(),
block_id.unwrap()
)
}
}
}
self.data.height = height;
self.data.round = round;
self.data.step = step;
self.data.block_id = block_id;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
use tendermint::{block, chain};

const EXAMPLE_BLOCK_ID: &str =
"26C0A41F3243C6BCD7AD2DFF8A8D83A71D29D307B5326C227F734A1A512FE47D";

const EXAMPLE_DOUBLE_SIGN_BLOCK_ID: &str =
"2470A41F3243C6BCD7AD2DFF8A8D83A71D29D307B5326C227F734A1A512FE47D";

#[test]
fn hrs_test() {
let mut last_sign_state = LastSignState {
data: LastSignData {
height: 1,
round: 1,
step: 0,
block_id: None,
},
file: File::create("/tmp/tmp_state.json").unwrap(),
_chain_id: "example-chain".parse::<chain::Id>().unwrap(),
};
assert_eq!(
last_sign_state.check_and_update_hrs(2, 0, 0, None).unwrap(),
()
)
}

#[test]
fn hrs_test_double_sign() {
let mut last_sign_state = LastSignState {
data: LastSignData {
height: 1,
round: 1,
step: 0,
block_id: Some(block::Id::from_str(EXAMPLE_BLOCK_ID).unwrap()),
},
file: File::create("/tmp/tmp_state.json").unwrap(),
_chain_id: "example-chain".parse::<chain::Id>().unwrap(),
};
let double_sign_block = block::Id::from_str(EXAMPLE_DOUBLE_SIGN_BLOCK_ID).unwrap();
let err = last_sign_state.check_and_update_hrs(1, 1, 1, Some(double_sign_block));

let double_sign_error = LastSignErrorKind::DoubleSign;

assert_eq!(
err.expect_err("Expect Double Sign error").0.kind(),
&double_sign_error
)
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ mod client;
mod commands;
mod config;
mod keyring;
mod last_sign_state;
mod rpc;
mod session;
mod unix_connection;
Expand Down
29 changes: 29 additions & 0 deletions src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use tendermint::{
use crate::{
error::KmsError,
keyring::KeyRing,
last_sign_state::LastSignState,
prost::Message,
rpc::{Request, Response, TendermintRequest},
unix_connection::UnixConnection,
Expand All @@ -34,6 +35,9 @@ pub struct Session<Connection> {

/// TCP connection to a validator node
connection: Connection,

/// Stateful double sign defense that is synced to disk
last_sign_state: LastSignState,
}

impl Session<SecretConnection<TcpStream>> {
Expand All @@ -50,10 +54,15 @@ impl Session<SecretConnection<TcpStream>> {
let signer = Ed25519Signer::from(secret_connection_key);
let public_key = SecretConnectionKey::from(ed25519::public_key(&signer)?);
let connection = SecretConnection::new(socket, &public_key, &signer)?;
let last_sign_state = LastSignState::load_state(
Path::new(&(chain_id.to_string() + "_priv_validator_state.json")),
chain_id,
)?;

Ok(Self {
chain_id,
connection,
last_sign_state,
})
}
}
Expand All @@ -68,10 +77,15 @@ impl Session<UnixConnection<UnixStream>> {

let socket = UnixStream::connect(socket_path)?;
let connection = UnixConnection::new(socket);
let last_sign_state = LastSignState::load_state(
Path::new(&(chain_id.as_ref().to_owned() + "_priv_validator_state.json")),
chain_id,
)?;

Ok(Self {
chain_id,
connection,
last_sign_state,
})
}
}
Expand Down Expand Up @@ -120,6 +134,21 @@ where
fn sign<T: TendermintRequest + Debug>(&mut self, mut request: T) -> Result<Response, KmsError> {
request.validate()?;

if let Some(cs) = request.consensus_state() {
match self.last_sign_state.check_and_update_hrs(
cs.height,
cs.round,
cs.step,
cs.block_id,
) {
Ok(()) => (),
Err(e) => {
debug! {"Double sign event: {}",e}
return Err(KmsError::from(e));
}
}
}
self.last_sign_state.sync_to_disk()?;
let mut to_sign = vec![];
request.sign_bytes(self.chain_id, &mut to_sign)?;

Expand Down
2 changes: 1 addition & 1 deletion tendermint-rs/src/amino_types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub use self::{
proposal::{SignProposalRequest, SignedProposalResponse, AMINO_NAME as PROPOSAL_AMINO_NAME},
remote_error::RemoteError,
secret_connection::AuthSigMessage,
signature::{SignableMsg, SignedMsgType},
signature::{ConsensusState, SignableMsg, SignedMsgType},
time::TimeMsg,
validate::ConsensusMessage,
vote::{SignVoteRequest, SignedVoteResponse, AMINO_NAME as VOTE_AMINO_NAME},
Expand Down
22 changes: 21 additions & 1 deletion tendermint-rs/src/amino_types/proposal.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use super::{
block_id::{BlockId, CanonicalBlockId, CanonicalPartSetHeader},
remote_error::RemoteError,
signature::{SignableMsg, SignedMsgType},
signature::{ConsensusState, SignableMsg, SignedMsgType},
time::TimeMsg,
validate::{ConsensusMessage, ValidationError, ValidationErrorKind::*},
};
use crate::block::ParseId;
use crate::{block, chain, error::Error};
use bytes::BufMut;
use prost::{EncodeError, Message};
Expand Down Expand Up @@ -129,6 +130,25 @@ impl SignableMsg for SignProposalRequest {
None => Err(MissingConsensusMessage.into()),
}
}
fn consensus_state(&self) -> Option<ConsensusState> {
match self.proposal {
Some(ref p) => Some(ConsensusState {
height: p.height,
round: p.round,
step: 3,
block_id: {
match p.block_id {
Some(ref b) => match b.parse_block_id() {
Ok(id) => Some(id),
Err(_) => None,
},
None => None,
}
},
}),
None => None,
}
}
}

impl ConsensusMessage for Proposal {
Expand Down
11 changes: 9 additions & 2 deletions tendermint-rs/src/amino_types/signature.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::validate::ValidationError;
use crate::chain;
use crate::{block, chain};
use bytes::BufMut;
use prost::{DecodeError, EncodeError};
use signatory::ed25519;
Expand All @@ -15,8 +15,15 @@ pub trait SignableMsg {

/// Set the Ed25519 signature on the underlying message
fn set_signature(&mut self, sig: &ed25519::Signature);

fn validate(&self) -> Result<(), ValidationError>;
fn consensus_state(&self) -> Option<ConsensusState>;
}

pub struct ConsensusState {
pub height: i64,
pub round: i64,
pub step: i8,
pub block_id: Option<block::Id>,
}

/// Signed message types. This follows:
Expand Down
Loading

0 comments on commit 52b7295

Please sign in to comment.