use hashgraph_like_consensus::{
events::BroadcastEventBus,
scope::ScopeID,
service::ConsensusService,
session::ConsensusConfig,
signing::{ConsensusSchemeError, ConsensusSignatureScheme},
storage::{ConsensusStorage, InMemoryConsensusStorage},
types::CreateProposalRequest,
utils::build_vote,
};
use sha2::{Digest, Sha256};
const STUB_IDENTITY_LEN: usize = 8;
#[derive(Debug, Clone)]
struct StubSigner {
identity: [u8; STUB_IDENTITY_LEN],
}
impl StubSigner {
fn new(identity: [u8; STUB_IDENTITY_LEN]) -> Self {
Self { identity }
}
fn expected_signature(identity: &[u8], payload: &[u8]) -> Vec<u8> {
let mut h = Sha256::new();
h.update(identity);
h.update(payload);
h.finalize().to_vec()
}
}
impl ConsensusSignatureScheme for StubSigner {
fn identity(&self) -> &[u8] {
&self.identity
}
async fn sign(&self, payload: &[u8]) -> Result<Vec<u8>, ConsensusSchemeError> {
Ok(Self::expected_signature(&self.identity, payload))
}
fn verify(
identity: &[u8],
payload: &[u8],
signature: &[u8],
) -> Result<bool, ConsensusSchemeError> {
if identity.len() != STUB_IDENTITY_LEN {
return Err(ConsensusSchemeError::Verify(format!(
"stub identity must be {STUB_IDENTITY_LEN} bytes, got {}",
identity.len()
)));
}
Ok(signature == Self::expected_signature(identity, payload))
}
}
type StubService = ConsensusService<
ScopeID,
InMemoryConsensusStorage<ScopeID>,
BroadcastEventBus<ScopeID>,
StubSigner,
>;
fn peer_service(
storage: &InMemoryConsensusStorage<ScopeID>,
bus: &BroadcastEventBus<ScopeID>,
signer: StubSigner,
) -> StubService {
StubService::new_with_components(storage.clone(), bus.clone(), signer, 10)
}
#[tokio::test]
async fn stub_scheme_reaches_consensus_without_ethereum_types() {
let storage = InMemoryConsensusStorage::<ScopeID>::new();
let bus = BroadcastEventBus::<ScopeID>::default();
let scope = ScopeID::from("stub-scope");
let owner = peer_service(&storage, &bus, StubSigner::new([1; STUB_IDENTITY_LEN]));
let voter_two = peer_service(&storage, &bus, StubSigner::new([2; STUB_IDENTITY_LEN]));
let voter_three = peer_service(&storage, &bus, StubSigner::new([3; STUB_IDENTITY_LEN]));
let proposal = owner
.create_proposal_with_config(
&scope,
CreateProposalRequest::new(
"stub-proposal".into(),
b"payload".to_vec(),
owner.signer().identity().to_vec(),
3,
60,
true,
)
.expect("valid proposal request"),
Some(ConsensusConfig::gossipsub()),
)
.await
.expect("proposal should be created");
owner
.cast_vote(&scope, proposal.proposal_id, true)
.await
.expect("owner vote");
voter_two
.cast_vote(&scope, proposal.proposal_id, true)
.await
.expect("voter two");
voter_three
.cast_vote(&scope, proposal.proposal_id, true)
.await
.expect("voter three");
let session = owner
.storage()
.get_session(&scope, proposal.proposal_id)
.await
.expect("get session")
.expect("session exists");
assert!(
session.get_consensus_result().expect("consensus reached"),
"3 YES votes via stub scheme should reach consensus"
);
}
#[tokio::test]
async fn stub_scheme_rejects_forged_signature() {
let storage = InMemoryConsensusStorage::<ScopeID>::new();
let bus = BroadcastEventBus::<ScopeID>::default();
let scope = ScopeID::from("stub-scope-forge");
let owner = peer_service(&storage, &bus, StubSigner::new([9; STUB_IDENTITY_LEN]));
let voter = StubSigner::new([10; STUB_IDENTITY_LEN]);
let proposal = owner
.create_proposal_with_config(
&scope,
CreateProposalRequest::new(
"stub-proposal".into(),
b"payload".to_vec(),
owner.signer().identity().to_vec(),
2,
60,
true,
)
.expect("valid proposal request"),
Some(ConsensusConfig::gossipsub()),
)
.await
.expect("proposal should be created");
let mut vote = build_vote(&proposal, true, &voter).await.expect("vote");
vote.signature.iter_mut().for_each(|b| *b ^= 0xFF);
let err = owner
.process_incoming_vote(&scope, vote)
.await
.expect_err("forged signature must be rejected");
assert!(
matches!(
err,
hashgraph_like_consensus::error::ConsensusError::InvalidVoteSignature
),
"expected InvalidVoteSignature, got {err:?}"
);
}