use crate::{
protocol::external::types::PeerServices,
zakura::{
select_zakura_protocol, ZakuraRejectReason, CONTROL_VERSION, P2P_V2_ALPN, PRELUDE_VERSION,
ZAKURA_PROTOCOL_VERSION_1,
},
};
pub const MAX_PINNED_ALPNS: usize = 8;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PeerProfile {
PlainZebra,
Zcashd,
ZakuraDisabled,
Zakura(PinnedZakuraProfile),
}
impl PeerProfile {
pub fn zakura_v1() -> Self {
Self::Zakura(PinnedZakuraProfile::v1())
}
pub fn zakura_v2_offering_v1() -> Self {
Self::Zakura(PinnedZakuraProfile {
protocol_min: ZAKURA_PROTOCOL_VERSION_1,
protocol_max: 2,
prelude_version: PRELUDE_VERSION,
control_version: CONTROL_VERSION,
capabilities: 0,
required_capabilities: 0,
alpns: vec![b"p2p-v2/2".to_vec(), P2P_V2_ALPN.to_vec()],
})
}
pub fn label(&self) -> &'static str {
match self {
Self::PlainZebra => "plain-zebra",
Self::Zcashd => "zcashd",
Self::ZakuraDisabled => "zakura-disabled",
Self::Zakura(profile) if profile.protocol_max == ZAKURA_PROTOCOL_VERSION_1 => {
"zakura-v1"
}
Self::Zakura(_) => "zakura-v2",
}
}
pub fn advertised_services(&self) -> PeerServices {
match self {
Self::PlainZebra | Self::Zcashd | Self::ZakuraDisabled => PeerServices::NODE_NETWORK,
Self::Zakura(_) => PeerServices::NODE_NETWORK | PeerServices::NODE_P2P_V2,
}
}
pub fn advertises_zakura(&self) -> bool {
self.advertised_services()
.contains(PeerServices::NODE_P2P_V2)
}
pub fn zakura(&self) -> Option<&PinnedZakuraProfile> {
match self {
Self::Zakura(profile) => Some(profile),
Self::PlainZebra | Self::Zcashd | Self::ZakuraDisabled => None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PinnedZakuraProfile {
pub protocol_min: u16,
pub protocol_max: u16,
pub prelude_version: u16,
pub control_version: u16,
pub capabilities: u64,
pub required_capabilities: u64,
pub alpns: Vec<Vec<u8>>,
}
impl PinnedZakuraProfile {
pub fn v1() -> Self {
Self {
protocol_min: ZAKURA_PROTOCOL_VERSION_1,
protocol_max: ZAKURA_PROTOCOL_VERSION_1,
prelude_version: PRELUDE_VERSION,
control_version: CONTROL_VERSION,
capabilities: 0,
required_capabilities: 0,
alpns: vec![P2P_V2_ALPN.to_vec()],
}
}
pub fn validate(&self) -> Result<(), PinnedProfileError> {
if self.protocol_min > self.protocol_max {
return Err(PinnedProfileError::InvalidProtocolRange);
}
if self.prelude_version == 0 {
return Err(PinnedProfileError::ZeroPreludeVersion);
}
if self.control_version == 0 {
return Err(PinnedProfileError::ZeroControlVersion);
}
if self.alpns.is_empty() {
return Err(PinnedProfileError::EmptyAlpns);
}
if self.alpns.len() > MAX_PINNED_ALPNS {
return Err(PinnedProfileError::TooManyAlpns {
actual: self.alpns.len(),
max: MAX_PINNED_ALPNS,
});
}
if self.required_capabilities & !self.capabilities != 0 {
return Err(PinnedProfileError::RequiresUnsupportedCapability);
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PinnedProfileError {
InvalidProtocolRange,
ZeroPreludeVersion,
ZeroControlVersion,
EmptyAlpns,
TooManyAlpns {
actual: usize,
max: usize,
},
RequiresUnsupportedCapability,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PinnedPeer {
profile: PeerProfile,
}
impl PinnedPeer {
pub fn new(profile: PeerProfile) -> Result<Self, PinnedProfileError> {
if let Some(zakura) = profile.zakura() {
zakura.validate()?;
}
Ok(Self { profile })
}
pub fn profile(&self) -> &PeerProfile {
&self.profile
}
pub fn negotiate(&self, remote: &PinnedPeer) -> PinnedNegotiation {
match (self.profile.zakura(), remote.profile.zakura()) {
(Some(local), Some(remote)) => {
if local.required_capabilities & !remote.capabilities != 0
|| remote.required_capabilities & !local.capabilities != 0
{
return PinnedNegotiation::NeutralReject(
ZakuraRejectReason::MissingRequiredCapability,
);
}
let Some(alpn) = select_alpn(&local.alpns, &remote.alpns) else {
return PinnedNegotiation::NeutralReject(
ZakuraRejectReason::IncompatibleZakuraProtocol,
);
};
match select_zakura_protocol(
local.protocol_min,
local.protocol_max,
remote.protocol_min,
remote.protocol_max,
) {
Ok(selected_protocol) => PinnedNegotiation::Upgrade {
selected_protocol,
alpn,
},
Err(reason) => PinnedNegotiation::NeutralReject(reason),
}
}
_ => PinnedNegotiation::LegacyOnly,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PinnedNegotiation {
LegacyOnly,
Upgrade {
selected_protocol: u16,
alpn: Vec<u8>,
},
NeutralReject(ZakuraRejectReason),
}
fn select_alpn(local: &[Vec<u8>], remote: &[Vec<u8>]) -> Option<Vec<u8>> {
local
.iter()
.find(|candidate| remote.iter().any(|remote_alpn| remote_alpn == *candidate))
.cloned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pinned_profiles_advertise_expected_services() {
assert!(!PeerProfile::PlainZebra.advertises_zakura());
assert!(!PeerProfile::Zcashd.advertises_zakura());
assert!(!PeerProfile::ZakuraDisabled.advertises_zakura());
assert!(PeerProfile::zakura_v1().advertises_zakura());
}
#[test]
fn pinned_negotiation_selects_highest_mutual_version_and_alpn() {
let v1 = PinnedPeer::new(PeerProfile::zakura_v1()).unwrap();
let v2 = PinnedPeer::new(PeerProfile::zakura_v2_offering_v1()).unwrap();
assert_eq!(
v2.negotiate(&v1),
PinnedNegotiation::Upgrade {
selected_protocol: 1,
alpn: P2P_V2_ALPN.to_vec(),
}
);
assert_eq!(
v2.negotiate(&v2),
PinnedNegotiation::Upgrade {
selected_protocol: 2,
alpn: b"p2p-v2/2".to_vec(),
}
);
}
#[test]
fn unknown_optional_capability_bits_do_not_block_negotiation() {
let v1 = PinnedPeer::new(PeerProfile::zakura_v1()).unwrap();
let future = PinnedPeer::new(PeerProfile::Zakura(PinnedZakuraProfile {
capabilities: 1 << 63,
..PinnedZakuraProfile::v1()
}))
.unwrap();
assert!(matches!(
v1.negotiate(&future),
PinnedNegotiation::Upgrade {
selected_protocol: 1,
..
}
));
}
#[test]
fn missing_required_capability_rejects_neutrally() {
let v1 = PinnedPeer::new(PeerProfile::zakura_v1()).unwrap();
let required = PinnedPeer::new(PeerProfile::Zakura(PinnedZakuraProfile {
capabilities: 1,
required_capabilities: 1,
..PinnedZakuraProfile::v1()
}))
.unwrap();
assert_eq!(
v1.negotiate(&required),
PinnedNegotiation::NeutralReject(ZakuraRejectReason::MissingRequiredCapability)
);
}
#[test]
fn unknown_alpns_reject_neutrally_without_panicking() {
let v1 = PinnedPeer::new(PeerProfile::zakura_v1()).unwrap();
let future_alpn = PinnedPeer::new(PeerProfile::Zakura(PinnedZakuraProfile {
alpns: vec![b"p2p-v2/future".to_vec()],
..PinnedZakuraProfile::v1()
}))
.unwrap();
assert_eq!(
v1.negotiate(&future_alpn),
PinnedNegotiation::NeutralReject(ZakuraRejectReason::IncompatibleZakuraProtocol)
);
}
#[test]
fn invalid_pinned_profiles_report_fixture_errors_not_wire_rejects() {
let error = PinnedPeer::new(PeerProfile::Zakura(PinnedZakuraProfile {
protocol_min: 2,
protocol_max: 1,
..PinnedZakuraProfile::v1()
}))
.unwrap_err();
assert_eq!(error, PinnedProfileError::InvalidProtocolRange);
}
}