use canton_proto::com::daml::ledger::api::v2::admin as pb;
#[derive(Clone, Debug)]
pub struct ExternalPartyTopology {
party_id: String,
public_key_fingerprint: String,
transactions: Vec<Vec<u8>>,
multi_hash: Vec<u8>,
}
impl ExternalPartyTopology {
pub(crate) fn from_response(
response: pb::GenerateExternalPartyTopologyResponse,
) -> canton_core::Result<Self> {
if response.multi_hash.is_empty() {
return Err(canton_core::Error::UnexpectedResponse(
"the participant returned no multi-hash to sign".to_string(),
));
}
if response.public_key_fingerprint.is_empty() {
return Err(canton_core::Error::UnexpectedResponse(
"the participant returned no key fingerprint, so nothing could \
sign as this party"
.to_string(),
));
}
if response.topology_transactions.is_empty() {
return Err(canton_core::Error::UnexpectedResponse(
"the participant returned no onboarding transactions, so there would be \
nothing to submit and the multi-hash would cover nothing"
.to_string(),
));
}
match response.party_id.rsplit_once("::") {
Some((_, namespace)) if namespace == response.public_key_fingerprint => {}
Some((_, namespace)) => {
return Err(canton_core::Error::UnexpectedResponse(format!(
"the participant offered party `{}`, whose namespace `{namespace}` is not the \
fingerprint `{}` of the key being registered — nothing this caller holds \
could sign as it",
response.party_id, response.public_key_fingerprint
)));
}
None => {
return Err(canton_core::Error::UnexpectedResponse(format!(
"the participant returned `{}`, which is not a party id",
response.party_id
)));
}
}
Ok(Self {
party_id: response.party_id,
public_key_fingerprint: response.public_key_fingerprint,
transactions: response.topology_transactions,
multi_hash: response.multi_hash,
})
}
#[must_use]
pub fn party_id(&self) -> &str {
&self.party_id
}
#[must_use]
pub fn public_key_fingerprint(&self) -> &str {
&self.public_key_fingerprint
}
#[must_use]
pub fn multi_hash(&self) -> &[u8] {
&self.multi_hash
}
#[must_use]
pub fn transactions(&self) -> &[Vec<u8>] {
&self.transactions
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::ExternalPartyTopology;
use canton_proto::com::daml::ledger::api::v2::admin as pb;
fn response(party_id: &str, fingerprint: &str) -> pb::GenerateExternalPartyTopologyResponse {
pb::GenerateExternalPartyTopologyResponse {
party_id: party_id.to_string(),
public_key_fingerprint: fingerprint.to_string(),
topology_transactions: vec![vec![1, 2, 3]],
multi_hash: vec![0x12, 0x20, 0xaa],
}
}
#[test]
fn a_party_whose_namespace_is_the_registered_key_is_accepted() {
let topology = ExternalPartyTopology::from_response(response("alice::1220ab", "1220ab"))
.expect("the namespace matches the key");
assert_eq!(topology.party_id(), "alice::1220ab");
assert_eq!(topology.public_key_fingerprint(), "1220ab");
}
#[test]
fn a_party_under_a_namespace_this_key_does_not_control_is_refused() {
let error = ExternalPartyTopology::from_response(response("alice::1220ff", "1220ab"))
.expect_err("the namespace is another key's");
let message = error.to_string();
assert!(message.contains("1220ff"), "name the namespace: {message}");
assert!(message.contains("1220ab"), "and the key: {message}");
}
#[test]
fn a_response_that_is_not_a_party_id_is_refused() {
let error = ExternalPartyTopology::from_response(response("alice", "1220ab"))
.expect_err("`alice` is not a party id");
assert!(error.to_string().contains("not a party id"), "{error}");
}
}