Skip to content
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 1 addition & 23 deletions crates/evm-node/src/rpc/get_certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,42 +17,20 @@
//! getCertificate RPC API implementation

use crate::rpc::common::{codes, invalid_params};
use arc_consensus_types::{Address, ValueId};
pub use arc_consensus_types::commit_http::HttpCommitCertificate as RpcCommitCertificate;
use async_trait::async_trait;
use backon::{ExponentialBuilder, Retryable};
use jsonrpsee::{
core::RpcResult,
types::{ErrorCode, ErrorObjectOwned},
};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use serde_with::{base64::Base64, serde_as};
use std::time::Duration;
use tracing::error;

/// Default maximum retry attempts for upstream certificate HTTP fetches.
const HTTP_MAX_RETRIES: usize = 3;

//TODO: These are copied from malachite-app, should we move them to types?
/// Signature entry within a commit certificate (base64 encoded signature bytes).
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcCommitSignature {
pub address: Address,
#[serde_as(as = "Base64")]
pub signature: Vec<u8>,
}

/// Commit certificate returned by `arc_getCertificate`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcCommitCertificate {
pub height: u64,
pub round: i64,
pub block_hash: ValueId,
#[serde(rename = "signatures")]
pub signatures: Vec<RpcCommitSignature>,
}

/// Core logic for the `arc_getCertificate` method: validates params, performs fetch, maps errors.
pub async fn rpc_get_certificate(
source: &dyn CertificateSource,
Expand Down
22 changes: 3 additions & 19 deletions crates/malachite-app/src/rpc/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use tokio::sync::mpsc::Sender;
use tracing::error;

use arc_consensus_types::{
commit_http::HttpCommitSignature,
evidence::{DoubleProposal, DoubleVote, StoredMisbehaviorEvidence, ValidatorEvidence},
signing::PublicKey,
Address, ArcContext, BlockHash, Height, Validator, ValidatorSet, Value, ValueId,
Expand All @@ -41,7 +42,7 @@ use malachitebft_app_channel::ConsensusRequest;
use malachitebft_app_channel::ConsensusRequestError;
use malachitebft_app_channel::NetworkRequest;
use malachitebft_core_state_machine::state::{RoundValue, State as MState};
use malachitebft_core_types::{CommitSignature, NilOrVal, VoteType};
use malachitebft_core_types::{NilOrVal, VoteType};
use malachitebft_network::PersistentPeerError;

use crate::request::{AppRequestError, CommitCertificateInfo, Status, TxAppReq};
Expand Down Expand Up @@ -353,29 +354,12 @@ struct RpcPendingProposalParts {
count: usize,
}

#[serde_as]
#[derive(Serialize)]
struct RpcCommitSignature {
address: Address,
#[serde_as(as = "Base64")]
signature: Vec<u8>,
}

impl From<CommitSignature<ArcContext>> for RpcCommitSignature {
fn from(sig: CommitSignature<ArcContext>) -> Self {
RpcCommitSignature {
address: sig.address,
signature: sig.signature.to_bytes().to_vec(),
}
}
}

#[derive(Serialize)]
pub(crate) struct RpcCommitCertificate {
height: u64,
round: i64,
block_hash: ValueId,
signatures: Vec<RpcCommitSignature>,
signatures: Vec<HttpCommitSignature>,
proposer: Address,
extended: bool,
}
Expand Down
65 changes: 6 additions & 59 deletions crates/malachite-app/src/rpc_sync/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,72 +31,19 @@ use alloy_rpc_types_eth::Block;
use bytes::Bytes;
use eyre::Context;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde::Deserialize;
use serde_json::{json, Value};
use serde_with::{base64::Base64, serde_as};
use tracing::debug;
use url::Url;

use malachitebft_core_types::utils::height::HeightRangeExt;
use malachitebft_core_types::{CommitCertificate, CommitSignature, Round};
use malachitebft_core_types::CommitCertificate;

use arc_consensus_types::signing::Signature;
use arc_consensus_types::{Address, ArcContext, Height, ValueId};
use arc_consensus_types::commit_http::HttpCommitCertificate;
use arc_consensus_types::{ArcContext, Height};

use crate::utils::pretty::Pretty;

/// RPC representation of a commit certificate
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcCommitCertificate {
pub height: u64,
pub round: i64,
pub block_hash: ValueId,
pub signatures: Vec<RpcCommitSignature>,
}

/// RPC representation of a commit signature
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcCommitSignature {
pub address: Address,
#[serde_as(as = "Base64")]
pub signature: Vec<u8>,
}

impl RpcCommitCertificate {
/// Convert to internal CommitCertificate type
pub fn into_commit_certificate(self) -> eyre::Result<CommitCertificate<ArcContext>> {
let signatures: eyre::Result<Vec<CommitSignature<ArcContext>>> =
self.signatures
.into_iter()
.map(|sig| {
let sig_bytes: [u8; 64] = sig.signature.try_into().map_err(|v: Vec<u8>| {
eyre::eyre!("Invalid signature length: {}", v.len())
})?;
Ok(CommitSignature {
address: sig.address,
signature: Signature::from_bytes(sig_bytes),
})
})
.collect();

let round_u32: u32 = self.round.try_into().map_err(|e| {
eyre::eyre!(
"Invalid round value {}, cannot convert to u32: {}",
self.round,
e
)
})?;

Ok(CommitCertificate {
height: Height::new(self.height),
round: Round::new(round_u32),
value_id: self.block_hash,
commit_signatures: signatures?,
})
}
}

/// A block with its associated data (certificate + payload)
#[derive(Debug, Clone)]
pub struct SyncedBlock {
Expand Down Expand Up @@ -176,7 +123,7 @@ impl RpcSyncClient {
let start = Instant::now();

let (cert_result, payload_result) = tokio::join!(
self.send_batch_request::<RpcCommitCertificate>(endpoint, &cert_requests),
self.send_batch_request::<HttpCommitCertificate>(endpoint, &cert_requests),
self.send_batch_request::<Block>(endpoint, &block_requests),
);

Expand Down Expand Up @@ -231,7 +178,7 @@ impl RpcSyncClient {
}

let certificate = rpc_cert
.into_commit_certificate()
.try_into_commit_certificate()
.wrap_err_with(|| format!("Failed to convert certificate at height {height}"))?;

// Convert Block to ExecutionPayloadV3, verifying hash consistency
Expand Down
1 change: 1 addition & 0 deletions crates/types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ malachitebft-sync = { workspace = true }
prost = { workspace = true }
rand = { workspace = true }
serde = { workspace = true, features = ["derive", "rc"] }
serde_with = { workspace = true, features = ["macros", "base64"] }
sha3 = { workspace = true }
signature = { workspace = true }
thiserror = { workspace = true }
Expand Down
151 changes: 151 additions & 0 deletions crates/types/src/commit_http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright 2026 Circle Internet Group, Inc. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! JSON types for commit certificates over HTTP or JSON-RPC.
//!
//! These structs match the minimal certificate payload returned by consensus
//! `GET /commit?height=…` and by JSON-RPC methods such as `arc_getCertificate`.

use malachitebft_core_types::{CommitCertificate, CommitSignature, Round};
use serde::{Deserialize, Serialize};
use serde_with::{base64::Base64, serde_as};

use crate::signing::Signature;
use crate::{Address, ArcContext, Height, ValueId};

/// Signature entry within a commit certificate (base64-encoded signature bytes).
#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HttpCommitSignature {
pub address: Address,
#[serde_as(as = "Base64")]
pub signature: Vec<u8>,
}

impl From<CommitSignature<ArcContext>> for HttpCommitSignature {
fn from(sig: CommitSignature<ArcContext>) -> Self {
Self {
address: sig.address,
signature: sig.signature.to_bytes().to_vec(),
}
}
}

/// Minimal commit certificate JSON (height, round, value id, signatures).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HttpCommitCertificate {
pub height: u64,
pub round: i64,
pub block_hash: ValueId,
pub signatures: Vec<HttpCommitSignature>,
}

impl HttpCommitCertificate {
/// Convert wire JSON into a [`CommitCertificate`].
pub fn try_into_commit_certificate(self) -> eyre::Result<CommitCertificate<ArcContext>> {
let signatures: eyre::Result<Vec<CommitSignature<ArcContext>>> = self
.signatures
.into_iter()
.map(|sig| {
let sig_bytes: [u8; 64] = sig.signature.try_into().map_err(|v: Vec<u8>| {
eyre::eyre!("Invalid signature length: {}", v.len())
})?;
Ok(CommitSignature {
address: sig.address,
signature: Signature::from_bytes(sig_bytes),
})
})
.collect();

let round_u32: u32 = self.round.try_into().map_err(|e| {
eyre::eyre!(
"Invalid round value {}, cannot convert to u32: {}",
self.round,
e
)
})?;

Ok(CommitCertificate {
height: Height::new(self.height),
round: Round::new(round_u32),
value_id: self.block_hash,
commit_signatures: signatures?,
})
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{Address, BlockHash};

#[test]
fn http_commit_certificate_json_roundtrip() {
let orig = HttpCommitCertificate {
height: 42,
round: 3,
block_hash: ValueId::new(BlockHash::ZERO),
signatures: vec![],
};
let json = serde_json::to_string(&orig).unwrap();
let back: HttpCommitCertificate = serde_json::from_str(&json).unwrap();
assert_eq!(orig, back);
}

#[test]
fn try_into_commit_certificate_empty_signatures() {
let wire = HttpCommitCertificate {
height: 10,
round: 2,
block_hash: ValueId::new(BlockHash::ZERO),
signatures: vec![],
};
let cert = wire.try_into_commit_certificate().unwrap();
assert_eq!(cert.height.as_u64(), 10);
assert_eq!(cert.round.as_i64(), 2);
assert_eq!(cert.value_id, ValueId::new(BlockHash::ZERO));
assert!(cert.commit_signatures.is_empty());
}

#[test]
fn try_into_rejects_short_signature() {
let wire = HttpCommitCertificate {
height: 1,
round: 0,
block_hash: ValueId::new(BlockHash::ZERO),
signatures: vec![HttpCommitSignature {
address: Address::default(),
signature: vec![0u8; 32],
}],
};
assert!(wire.try_into_commit_certificate().is_err());
}

#[test]
fn http_commit_signature_json_roundtrip() {
let mut b = [0u8; 64];
b[0] = 1;
b[63] = 2;
let cs = CommitSignature {
address: Address::default(),
signature: Signature::from_bytes(b),
};
let http = HttpCommitSignature::from(cs);
let json = serde_json::to_string(&http).unwrap();
let back: HttpCommitSignature = serde_json::from_str(&json).unwrap();
assert_eq!(http, back);
}
}
1 change: 1 addition & 0 deletions crates/types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
mod address;
mod aliases;
mod certificate;
pub mod commit_http;
mod config;
mod consensus_params;
mod context;
Expand Down