Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fraud): Add fraud proof trait and byzantine encoding fraud #32

Merged
merged 7 commits into from
Jul 17, 2023
Merged
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
497 changes: 285 additions & 212 deletions Cargo.lock

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions proto/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const DEFAULT: &str = r#"#[serde(default)]"#;
const SERIALIZED: &str = r#"#[derive(::serde::Deserialize, ::serde::Serialize)]"#;
const BASE64STRING: &str =
r#"#[serde(with = "tendermint_proto::serializers::bytes::base64string")]"#;
const QUOTED: &str = r#"#[serde(with = "tendermint_proto::serializers::from_str")]"#;
const VEC_BASE64STRING: &str =
r#"#[serde(with = "tendermint_proto::serializers::bytes::vec_base64string")]"#;
const PASCAL_CASE: &str = r#"#[serde(rename_all = "PascalCase")]"#;
Expand Down Expand Up @@ -32,6 +33,9 @@ pub static CUSTOM_TYPE_ATTRIBUTES: &[(&str, &str)] = &[
(".cosmos.staking.v1beta1.UnbondingDelegation", SERIALIZED),
(".cosmos.staking.v1beta1.UnbondingDelegationEntry", SERIALIZED),
(".header.pb.ExtendedHeader", SERIALIZED),
(".share.eds.byzantine.pb.BadEncoding", SERIALIZED),
(".share.eds.byzantine.pb.MerkleProof", SERIALIZED),
(".share.eds.byzantine.pb.Share", SERIALIZED),
(".share.p2p.shrex.nd.Proof", SERIALIZED),
(".share.p2p.shrex.nd.Row", SERIALIZED),
(".share.p2p.shrex.nd.Row", PASCAL_CASE),
Expand All @@ -45,6 +49,16 @@ pub static CUSTOM_FIELD_ATTRIBUTES: &[(&str, &str)] = &[
(".cosmos.base.query.v1beta1.PageResponse.next_key", BASE64STRING),
(".cosmos.staking.v1beta1.RedelegationEntry.completion_time", OPTION_TIMESTAMP),
(".cosmos.staking.v1beta1.UnbondingDelegationEntry.completion_time", OPTION_TIMESTAMP),
(
".share.eds.byzantine.pb.MerkleProof.nodes",
VEC_BASE64STRING,
),
(".share.eds.byzantine.pb.MerkleProof.leaf_hash", DEFAULT),
(
".share.eds.byzantine.pb.MerkleProof.leaf_hash",
BASE64STRING,
),
(".share.eds.byzantine.pb.BadEncoding.axis", QUOTED),
(".share.p2p.shrex.nd.Proof.nodes", VEC_BASE64STRING),
(".share.p2p.shrex.nd.Proof.hashleaf", DEFAULT),
(".share.p2p.shrex.nd.Proof.hashleaf", BASE64STRING),
Expand Down Expand Up @@ -82,6 +96,7 @@ fn main() -> Result<()> {
"vendor/celestia/da/data_availability_header.proto",
"vendor/header/pb/extended_header.proto",
"vendor/share/p2p/shrexnd/pb/share.proto",
"vendor/share/eds/byzantine/pb/share.proto",
"vendor/cosmos/base/v1beta1/coin.proto",
"vendor/cosmos/base/abci/v1beta1/abci.proto",
"vendor/cosmos/staking/v1beta1/query.proto",
Expand Down
4 changes: 2 additions & 2 deletions rpc/tests/share.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ async fn get_shares_by_namespace() {
.await
.unwrap();

let seq_len =
&ns_shares.rows[0].shares[0].data[SHARE_INFO_BYTES..SHARE_INFO_BYTES + SEQUENCE_LEN_BYTES];
let seq_len = &ns_shares.rows[0].shares[0].data()
[SHARE_INFO_BYTES..SHARE_INFO_BYTES + SEQUENCE_LEN_BYTES];
let seq_len = u32::from_be_bytes(seq_len.try_into().unwrap());
assert_eq!(seq_len as usize, data.len());

Expand Down
1 change: 1 addition & 0 deletions types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ bytes = "1.4.0"
celestia-proto = { workspace = true }
const_format = "0.2.31"
enum_dispatch = "0.3.12"
cid = { version = "0.10.1", default-features = false, features = ["std"] }
nmt-rs = { workspace = true }
ruint = { version = "1.8.0", features = ["serde"] }
serde = { version = "1.0.164", features = ["derive"] }
Expand Down
299 changes: 299 additions & 0 deletions types/src/byzantine.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
use celestia_proto::share::eds::byzantine::pb::BadEncoding as RawBadEncodingFraudProof;
use celestia_proto::share::eds::byzantine::pb::MerkleProof as RawEdsProof;
use celestia_proto::share::eds::byzantine::pb::Share as RawShareWithProof;
use celestia_proto::share::p2p::shrex::nd::Proof as RawShrexProof;
use cid::multihash::MultihashGeneric;
use cid::CidGeneric;
use serde::{Deserialize, Serialize};
use tendermint::hash::SHA256_HASH_SIZE;
use tendermint::{block::Height, Hash};
use tendermint_proto::Protobuf;

use crate::bail_validation;
use crate::consts::appconsts;
use crate::fraud_proof::FraudProof;
use crate::nmt::NamespacedHash;
use crate::nmt::{Namespace, NamespaceProof, NamespacedHashExt, NS_SIZE};
use crate::rsmt2d::Axis;
use crate::{Error, ExtendedHeader, Result, Share};

pub const MULTIHASH_NMT_CODEC_CODE: u64 = 0x7700;
pub const MULTIHASH_SHA256_NAMESPACE_FLAGGED_CODE: u64 = 0x7701;
pub const MULTIHASH_SHA256_NAMESPACE_FLAGGED_SIZE: usize = 2 * NS_SIZE + SHA256_HASH_SIZE;

type Cid = CidGeneric<MULTIHASH_SHA256_NAMESPACE_FLAGGED_SIZE>;
type Multihash = MultihashGeneric<MULTIHASH_SHA256_NAMESPACE_FLAGGED_SIZE>;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(
try_from = "RawBadEncodingFraudProof",
into = "RawBadEncodingFraudProof"
)]
pub struct BadEncodingFraudProof {
header_hash: Hash,
block_height: Height,
// ShareWithProof contains all shares from row or col.
// Shares that did not pass verification in rsmt2d will be nil.
// For non-nil shares MerkleProofs are computed.
shares: Vec<ShareWithProof>,
// Index represents the row/col index where ErrByzantineRow/ErrByzantineColl occurred.
index: usize,
// Axis represents the axis that verification failed on.
axis: Axis,
}

impl FraudProof for BadEncodingFraudProof {
const TYPE: &'static str = "badencoding";

fn header_hash(&self) -> Hash {
self.header_hash
}

fn height(&self) -> Height {
self.block_height
}

fn validate(&self, header: &ExtendedHeader) -> Result<()> {
if header.height() != self.height() {
bail_validation!(
"header height ({}) != fraud proof height ({})",
header.height(),
self.height()
);
}

let merkle_row_roots = &header.dah.row_roots;
let merkle_col_roots = &header.dah.column_roots;

// NOTE: shouldn't ever happen as header should be validated before
if merkle_row_roots.len() != merkle_col_roots.len() {
bail_validation!(
"dah rows len ({}) != dah columns len ({})",
merkle_row_roots.len(),
merkle_col_roots.len(),
);
}

if self.index >= merkle_row_roots.len() {
bail_validation!(
"fraud proof index ({}) >= dah rows len ({})",
self.index,
merkle_row_roots.len()
);
}

if self.shares.len() != merkle_row_roots.len() {
bail_validation!(
"fraud proof shares len ({}) != dah rows len ({})",
self.shares.len(),
merkle_row_roots.len()
);
}

let root = match self.axis {
Axis::Row => merkle_row_roots[self.index],
Axis::Col => merkle_col_roots[self.index],
};

// verify if the root can be converted to a cid and back
let mh = Multihash::wrap(MULTIHASH_SHA256_NAMESPACE_FLAGGED_CODE, &root.to_array())?;
let cid = Cid::new_v1(MULTIHASH_NMT_CODEC_CODE, mh);
let root = NamespacedHash::try_from(cid.hash().digest())?;

// verify that Merkle proofs correspond to particular shares.
for share in &self.shares {
share
.proof
.verify_range(
&root,
&[share.leaf.share.as_ref()],
share.leaf.namespace.into(),
)
.map_err(Error::RangeProofError)?;
}

// TODO: Add leopard reed solomon decoding and encoding of shares
// and verify the nmt roots.

Ok(())
}
}

// TODO: this is not a Share but an Nmt Leaf, so it has it's namespace prepended.
// It seems intentional in Celestia code, discuss with them what to do with this naming.
#[derive(Debug, Clone, PartialEq)]
struct ShareWithProof {
leaf: NmtLeaf,
proof: NamespaceProof,
}

#[derive(Debug, Clone, PartialEq)]
struct NmtLeaf {
namespace: Namespace,
share: Share,
}

impl NmtLeaf {
pub fn new(bytes: Vec<u8>) -> Result<Self> {
if bytes.len() != appconsts::SHARE_SIZE + NS_SIZE {
return Err(Error::InvalidNmtLeafSize(bytes.len()));
}

let (namespace, share) = bytes.split_at(NS_SIZE);

Ok(Self {
namespace: Namespace::from_raw(namespace)?,
share: Share::from_raw(share)?,
})
}

pub fn to_vec(&self) -> Vec<u8> {
let mut bytes = self.namespace.as_bytes().to_vec();
bytes.extend_from_slice(self.share.as_ref());
bytes
}
}

impl TryFrom<RawShareWithProof> for ShareWithProof {
type Error = Error;

fn try_from(value: RawShareWithProof) -> Result<Self, Self::Error> {
let leaf = NmtLeaf::new(value.data)?;

let proof = value.proof.ok_or(Error::MissingProof)?;
let proof = NamespaceProof::try_from(eds_proof_to_shrex(proof))?;

if proof.is_of_absence() {
return Err(Error::WrongProofType);
}

Ok(Self { leaf, proof })
}
}

impl From<ShareWithProof> for RawShareWithProof {
fn from(value: ShareWithProof) -> Self {
let proof = RawShrexProof::from(value.proof);
RawShareWithProof {
data: value.leaf.to_vec(),
proof: Some(shrex_proof_to_eds(proof)),
}
}
}

impl Protobuf<RawBadEncodingFraudProof> for BadEncodingFraudProof {}

impl TryFrom<RawBadEncodingFraudProof> for BadEncodingFraudProof {
type Error = Error;

fn try_from(value: RawBadEncodingFraudProof) -> Result<Self, Self::Error> {
Ok(Self {
header_hash: value.header_hash.try_into()?,
block_height: value.height.try_into()?,
shares: value
.shares
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?,
index: value.index as usize,
axis: value.axis.try_into()?,
})
}
}

impl From<BadEncodingFraudProof> for RawBadEncodingFraudProof {
fn from(value: BadEncodingFraudProof) -> Self {
RawBadEncodingFraudProof {
header_hash: value.header_hash.into(),
height: value.block_height.into(),
shares: value.shares.into_iter().map(Into::into).collect(),
index: value.index as u32,
axis: value.axis as i32,
}
}
}

fn eds_proof_to_shrex(eds_proof: RawEdsProof) -> RawShrexProof {
RawShrexProof {
start: eds_proof.start,
end: eds_proof.end,
nodes: eds_proof.nodes,
hashleaf: eds_proof.leaf_hash,
}
}

fn shrex_proof_to_eds(shrex_proof: RawShrexProof) -> RawEdsProof {
RawEdsProof {
start: shrex_proof.start,
end: shrex_proof.end,
nodes: shrex_proof.nodes,
leaf_hash: shrex_proof.hashleaf,
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::fraud_proof::Proof;

fn honest_befp() -> (BadEncodingFraudProof, ExtendedHeader) {
let befp_json = include_str!("../test_data/fraud/honest_bad_encoding_fraud_proof.json");
let eh_json = include_str!("../test_data/fraud/honest_bad_encoding_extended_header.json");
let Proof::BadEncoding(proof) = serde_json::from_str(befp_json).unwrap();

(proof, serde_json::from_str(eh_json).unwrap())
}

fn fake_befp() -> (BadEncodingFraudProof, ExtendedHeader) {
let befp_json = include_str!("../test_data/fraud/fake_bad_encoding_fraud_proof.json");
let eh_json = include_str!("../test_data/fraud/fake_bad_encoding_extended_header.json");
let Proof::BadEncoding(proof) = serde_json::from_str(befp_json).unwrap();

(proof, serde_json::from_str(eh_json).unwrap())
}

#[test]
fn validate_honest_befp() {
let (proof, eh) = honest_befp();
proof.validate(&eh).unwrap();
}

#[test]
fn validate_befp_wrong_height() {
let (proof, mut eh) = honest_befp();
eh.header.height = 999u32.into();

proof.validate(&eh).unwrap_err();
}

#[test]
fn validate_befp_wrong_roots_square() {
let (proof, mut eh) = honest_befp();
eh.dah.row_roots = vec![];

proof.validate(&eh).unwrap_err();
}

#[test]
fn validate_befp_wrong_index() {
let (mut proof, eh) = honest_befp();
proof.index = 999;

proof.validate(&eh).unwrap_err();
}

#[test]
fn validate_befp_wrong_shares() {
let (mut proof, eh) = honest_befp();
proof.shares = vec![];

proof.validate(&eh).unwrap_err();
}

#[test]
#[ignore = "TODO: we can't catch fake proofs without rebuilding the row using reedsolomon"]
fn validate_fake_befp() {
let (proof, eh) = fake_befp();
proof.validate(&eh).unwrap_err();
}
}
Loading