From 15c39870e5228ea8cf226592b2c42c9ac3ef2988 Mon Sep 17 00:00:00 2001 From: crazywriter1 Date: Thu, 23 Apr 2026 23:10:57 +0300 Subject: [PATCH] refactor(types): centralize HTTP commit certificate JSON wire types Add arc_consensus_types::commit_http (HttpCommitCertificate, HttpCommitSignature) and try_into_commit_certificate. Use from arc-evm-node get_certificate and malachite-app rpc_sync; RpcCommitCertificate HTTP response uses HttpCommitSignature. Add commit_http unit tests; serde_with dep + Cargo.lock. --- Cargo.lock | 1 + crates/evm-node/src/rpc/get_certificate.rs | 24 +--- crates/malachite-app/src/rpc/types.rs | 22 +-- crates/malachite-app/src/rpc_sync/client.rs | 65 +-------- crates/types/Cargo.toml | 1 + crates/types/src/commit_http.rs | 151 ++++++++++++++++++++ crates/types/src/lib.rs | 1 + 7 files changed, 164 insertions(+), 101 deletions(-) create mode 100644 crates/types/src/commit_http.rs diff --git a/Cargo.lock b/Cargo.lock index ca0a438..74ba18d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1086,6 +1086,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", + "serde_with", "sha3 0.10.8", "signature 2.2.0", "thiserror 2.0.18", diff --git a/crates/evm-node/src/rpc/get_certificate.rs b/crates/evm-node/src/rpc/get_certificate.rs index 167a380..901f536 100644 --- a/crates/evm-node/src/rpc/get_certificate.rs +++ b/crates/evm-node/src/rpc/get_certificate.rs @@ -17,7 +17,7 @@ //! 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::{ @@ -25,34 +25,12 @@ use jsonrpsee::{ 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, -} - -/// 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, -} - /// Core logic for the `arc_getCertificate` method: validates params, performs fetch, maps errors. pub async fn rpc_get_certificate( source: &dyn CertificateSource, diff --git a/crates/malachite-app/src/rpc/types.rs b/crates/malachite-app/src/rpc/types.rs index 4f5217e..be05ab2 100644 --- a/crates/malachite-app/src/rpc/types.rs +++ b/crates/malachite-app/src/rpc/types.rs @@ -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, @@ -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}; @@ -353,29 +354,12 @@ struct RpcPendingProposalParts { count: usize, } -#[serde_as] -#[derive(Serialize)] -struct RpcCommitSignature { - address: Address, - #[serde_as(as = "Base64")] - signature: Vec, -} - -impl From> for RpcCommitSignature { - fn from(sig: CommitSignature) -> 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, + signatures: Vec, proposer: Address, extended: bool, } diff --git a/crates/malachite-app/src/rpc_sync/client.rs b/crates/malachite-app/src/rpc_sync/client.rs index a739710..d97e663 100644 --- a/crates/malachite-app/src/rpc_sync/client.rs +++ b/crates/malachite-app/src/rpc_sync/client.rs @@ -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, -} - -/// 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, -} - -impl RpcCommitCertificate { - /// Convert to internal CommitCertificate type - pub fn into_commit_certificate(self) -> eyre::Result> { - let signatures: eyre::Result>> = - self.signatures - .into_iter() - .map(|sig| { - let sig_bytes: [u8; 64] = sig.signature.try_into().map_err(|v: Vec| { - 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 { @@ -176,7 +123,7 @@ impl RpcSyncClient { let start = Instant::now(); let (cert_result, payload_result) = tokio::join!( - self.send_batch_request::(endpoint, &cert_requests), + self.send_batch_request::(endpoint, &cert_requests), self.send_batch_request::(endpoint, &block_requests), ); @@ -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 diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index 47be2d4..5b52c31 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -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 } diff --git a/crates/types/src/commit_http.rs b/crates/types/src/commit_http.rs new file mode 100644 index 0000000..687073a --- /dev/null +++ b/crates/types/src/commit_http.rs @@ -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, +} + +impl From> for HttpCommitSignature { + fn from(sig: CommitSignature) -> 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, +} + +impl HttpCommitCertificate { + /// Convert wire JSON into a [`CommitCertificate`]. + pub fn try_into_commit_certificate(self) -> eyre::Result> { + let signatures: eyre::Result>> = self + .signatures + .into_iter() + .map(|sig| { + let sig_bytes: [u8; 64] = sig.signature.try_into().map_err(|v: Vec| { + 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); + } +} diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index e8d76c8..32fbdc4 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -21,6 +21,7 @@ mod address; mod aliases; mod certificate; +pub mod commit_http; mod config; mod consensus_params; mod context;