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

State tracking for double sign protection #193

Merged
merged 20 commits into from
Mar 8, 2019
Merged
Show file tree
Hide file tree
Changes from 12 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
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,
io::prelude::*,
path::Path,
};
use tendermint::{block, chain};

#[derive(Serialize, Deserialize)]
struct LastSignData {
pub height: i64,
pub round: i64,
pub step: i8,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can these ever actually be negative?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not entirely sure. I have a vague memory that in Tendermint sometimes these values get set at -1 as a marker.

But I haven't actually looked through all the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also those are the types in the Tendermint implementation.

Copy link
Contributor

Choose a reason for hiding this comment

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

As far as I know the only thing that can be negative is the Proposal.POLRound (can be -1 as @zmanian mentioned). The other fields can not be negative. But yeah the types are all signed in the tendermint golang code ... Not sure we need to follow this tho.

Copy link
Contributor

Choose a reason for hiding this comment

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

I might suggest using unsigned integers for anything that isn't explicitly negative, and potentially Option in lieu of -1

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: File::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).unwrap().as_ref())?;
self.file.sync_all()?;
Copy link
Contributor

@tarcieri tarcieri Mar 6, 2019

Choose a reason for hiding this comment

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

This could be made a bit more atomic by writing the new state to a separate file (e.g. .chainid_priv_validator_state.json.tmp) then replacing/overwriting the other file (e.g. with fs::rename)

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 @@ -30,6 +30,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.as_ref().to_owned() + "_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,
Copy link
Contributor

Choose a reason for hiding this comment

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

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