Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate from protoc to quick-protobuf #3066

Closed
wants to merge 15 commits into from
6 changes: 6 additions & 0 deletions core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 0.38.1 [unreleased]

- Migrate from `prost` to `quick-protobuf`. This removes `protoc` dependency. See [PR 3024].

[PR 3024]: https://github.com/libp2p/rust-libp2p/issues/3024

# 0.38.0

- Remove deprecated functions `StreamMuxerExt::next_{inbound,outbound}`. See [PR 3031].
Expand Down
5 changes: 1 addition & 4 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ multistream-select = { version = "0.12.1", path = "../misc/multistream-select" }
p256 = { version = "0.11.1", default-features = false, features = ["ecdsa", "std"], optional = true }
parking_lot = "0.12.0"
pin-project = "1.0.0"
prost = "0.11"
once_cell = "1.16.0"
quick-protobuf = "0.8"
rand = "0.8"
rw-stream-sink = { version = "0.3.0", path = "../misc/rw-stream-sink" }
sec1 = { version = "0.3.0", features = ["std"] } # Activate `std` feature until https://github.com/RustCrypto/traits/pull/1131 is released.
Expand All @@ -54,9 +54,6 @@ quickcheck = { package = "quickcheck-ext", path = "../misc/quickcheck-ext" }
rmp-serde = "1.0"
serde_json = "1.0"

[build-dependencies]
prost-build = "0.11"

[features]
secp256k1 = [ "libsecp256k1" ]
ecdsa = [ "p256" ]
Expand Down
31 changes: 0 additions & 31 deletions core/build.rs

This file was deleted.

92 changes: 38 additions & 54 deletions core/src/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub mod error;

use self::error::*;
use crate::{keys_proto, PeerId};
use std::borrow::Cow;
use std::convert::{TryFrom, TryInto};

/// Identity keypair of a node.
Expand Down Expand Up @@ -147,12 +148,10 @@ impl Keypair {

/// Encode a private key as protobuf structure.
pub fn to_protobuf_encoding(&self) -> Result<Vec<u8>, DecodingError> {
use prost::Message;

let pk = match self {
Self::Ed25519(data) => keys_proto::PrivateKey {
r#type: keys_proto::KeyType::Ed25519.into(),
data: data.encode().into(),
Type: keys_proto::KeyType::Ed25519,
Data: Cow::from(data.encode().to_vec()),
},
#[cfg(all(feature = "rsa", not(target_arch = "wasm32")))]
Self::Rsa(_) => return Err(DecodingError::encoding_unsupported("RSA")),
Expand All @@ -162,35 +161,31 @@ impl Keypair {
Self::Ecdsa(_) => return Err(DecodingError::encoding_unsupported("ECDSA")),
};

Ok(pk.encode_to_vec())
Ok(quick_protobuf::serialize_into_vec(&pk).expect("Encoding to succeed."))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is infallible by looking at the code. Unfortunately, I have not found a concrete answer in the docs or issues.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe so too. One error I could find is https://docs.rs/quick-protobuf/latest/src/quick_protobuf/writer.rs.html#379 which can't happen if the Vec is allocated correctly.

The other error path comes from lines like https://github.com/tafia/quick-protobuf/blob/d977371e05170a016f03a80512b2a925468e3a1a/quick-protobuf/examples/pb_rs/data_types.rs#L199 which end up being the same error case as above so I think we are good.

I believe this function could actually be made infallible in quick-protobuf but I am not sure.

}

/// Decode a private key from a protobuf structure and parse it as a [`Keypair`].
pub fn from_protobuf_encoding(bytes: &[u8]) -> Result<Keypair, DecodingError> {
use prost::Message;

let mut private_key = keys_proto::PrivateKey::decode(bytes)
.map_err(|e| DecodingError::bad_protobuf("private key bytes", e))
.map(zeroize::Zeroizing::new)?;

let key_type = keys_proto::KeyType::from_i32(private_key.r#type)
.ok_or_else(|| DecodingError::unknown_key_type(private_key.r#type))?;
let mut private_key =
quick_protobuf::deserialize_from_slice::<keys_proto::PrivateKey>(bytes)
.map_err(|e| DecodingError::bad_protobuf("private key bytes", e))
.map(zeroize::Zeroizing::new)?;

match key_type {
match private_key.Type {
keys_proto::KeyType::Ed25519 => {
ed25519::Keypair::decode(&mut private_key.data).map(Keypair::Ed25519)
ed25519::Keypair::decode(private_key.Data.to_mut()).map(Keypair::Ed25519)
}
keys_proto::KeyType::Rsa => Err(DecodingError::decoding_unsupported("RSA")),
keys_proto::KeyType::RSA => Err(DecodingError::decoding_unsupported("RSA")),
keys_proto::KeyType::Secp256k1 => Err(DecodingError::decoding_unsupported("secp256k1")),
keys_proto::KeyType::Ecdsa => Err(DecodingError::decoding_unsupported("ECDSA")),
keys_proto::KeyType::ECDSA => Err(DecodingError::decoding_unsupported("ECDSA")),
}
}
}

impl zeroize::Zeroize for keys_proto::PrivateKey {
impl zeroize::Zeroize for keys_proto::PrivateKey<'_> {
fn zeroize(&mut self) {
self.r#type.zeroize();
self.data.zeroize();
self.Type = keys_proto::KeyType::RSA;
self.Data.to_mut().zeroize();
}
}

Expand Down Expand Up @@ -232,23 +227,15 @@ impl PublicKey {
/// Encode the public key into a protobuf structure for storage or
/// exchange with other nodes.
pub fn to_protobuf_encoding(&self) -> Vec<u8> {
use prost::Message;

let public_key = keys_proto::PublicKey::from(self);

let mut buf = Vec::with_capacity(public_key.encoded_len());
public_key
.encode(&mut buf)
.expect("Vec<u8> provides capacity as needed");
buf
quick_protobuf::serialize_into_vec(&public_key).expect("Encoding to succeed.")
}

/// Decode a public key from a protobuf structure, e.g. read from storage
/// or received from another node.
pub fn from_protobuf_encoding(bytes: &[u8]) -> Result<PublicKey, DecodingError> {
use prost::Message;

let pubkey = keys_proto::PublicKey::decode(bytes)
let pubkey: keys_proto::PublicKey = quick_protobuf::deserialize_from_slice(bytes)
.map_err(|e| DecodingError::bad_protobuf("public key bytes", e))?;

pubkey.try_into()
Expand All @@ -260,67 +247,64 @@ impl PublicKey {
}
}

impl From<&PublicKey> for keys_proto::PublicKey {
impl From<&PublicKey> for keys_proto::PublicKey<'_> {
fn from(key: &PublicKey) -> Self {
match key {
PublicKey::Ed25519(key) => keys_proto::PublicKey {
r#type: keys_proto::KeyType::Ed25519 as i32,
data: key.encode().to_vec(),
Type: keys_proto::KeyType::Ed25519,
Data: Cow::from(key.encode().to_vec()),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the usage of Cow here would actually allow us to remove an allocation. A Ed25519 key in particular can also be viewed as a byte-slice so we could use Cow::Borrowed here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seem like encode() returns an array which creates a temporary that goes out of scope so we can't remove the allocation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to add pub(crate) fn as_bytes(&self) -> &[u8] to ed25519::PublicKey which then allows the use of Cow::Borrowed.

},
#[cfg(all(feature = "rsa", not(target_arch = "wasm32")))]
PublicKey::Rsa(key) => keys_proto::PublicKey {
r#type: keys_proto::KeyType::Rsa as i32,
data: key.encode_x509(),
Type: keys_proto::KeyType::RSA,
Data: Cow::from(key.encode_x509()),
},
#[cfg(feature = "secp256k1")]
PublicKey::Secp256k1(key) => keys_proto::PublicKey {
r#type: keys_proto::KeyType::Secp256k1 as i32,
data: key.encode().to_vec(),
Type: keys_proto::KeyType::Secp256k1,
Data: Cow::from(key.encode().to_vec()),
},
#[cfg(feature = "ecdsa")]
PublicKey::Ecdsa(key) => keys_proto::PublicKey {
r#type: keys_proto::KeyType::Ecdsa as i32,
data: key.encode_der(),
Type: keys_proto::KeyType::ECDSA,
Data: Cow::from(key.encode_der()),
},
}
}
}

impl TryFrom<keys_proto::PublicKey> for PublicKey {
impl TryFrom<keys_proto::PublicKey<'_>> for PublicKey {
type Error = DecodingError;

fn try_from(pubkey: keys_proto::PublicKey) -> Result<Self, Self::Error> {
let key_type = keys_proto::KeyType::from_i32(pubkey.r#type)
.ok_or_else(|| DecodingError::unknown_key_type(pubkey.r#type))?;

match key_type {
match pubkey.Type {
keys_proto::KeyType::Ed25519 => {
ed25519::PublicKey::decode(&pubkey.data).map(PublicKey::Ed25519)
ed25519::PublicKey::decode(&pubkey.Data).map(PublicKey::Ed25519)
}
#[cfg(all(feature = "rsa", not(target_arch = "wasm32")))]
keys_proto::KeyType::Rsa => {
rsa::PublicKey::decode_x509(&pubkey.data).map(PublicKey::Rsa)
keys_proto::KeyType::RSA => {
rsa::PublicKey::decode_x509(&pubkey.Data).map(PublicKey::Rsa)
}
#[cfg(any(not(feature = "rsa"), target_arch = "wasm32"))]
keys_proto::KeyType::Rsa => {
keys_proto::KeyType::RSA => {
log::debug!("support for RSA was disabled at compile-time");
Err(DecodingError::missing_feature("rsa"))
}
#[cfg(feature = "secp256k1")]
keys_proto::KeyType::Secp256k1 => {
secp256k1::PublicKey::decode(&pubkey.data).map(PublicKey::Secp256k1)
secp256k1::PublicKey::decode(&pubkey.Data).map(PublicKey::Secp256k1)
}
#[cfg(not(feature = "secp256k1"))]
keys_proto::KeyType::Secp256k1 => {
log::debug!("support for secp256k1 was disabled at compile-time");
Err(DecodingError::missing_feature("secp256k1"))
}
#[cfg(feature = "ecdsa")]
keys_proto::KeyType::Ecdsa => {
ecdsa::PublicKey::decode_der(&pubkey.data).map(PublicKey::Ecdsa)
keys_proto::KeyType::ECDSA => {
ecdsa::PublicKey::decode_der(&pubkey.Data).map(PublicKey::Ecdsa)
}
#[cfg(not(feature = "ecdsa"))]
keys_proto::KeyType::Ecdsa => {
keys_proto::KeyType::ECDSA => {
log::debug!("support for ECDSA was disabled at compile-time");
Err(DecodingError::missing_feature("ecdsa"))
}
Expand Down Expand Up @@ -349,9 +333,9 @@ mod tests {
#[test]
fn keypair_from_protobuf_encoding() {
// E.g. retrieved from an IPFS config file.
let base_64_encoded = "CAESQL6vdKQuznQosTrW7FWI9At+XX7EBf0BnZLhb6w+N+XSQSdfInl6c7U4NuxXJlhKcRBlBw9d0tj2dfBIVf6mcPA=";
let base_64_encoded = "RAgBEkC+r3SkLs50KLE61uxViPQLfl1+xAX9AZ2S4W+sPjfl0kEnXyJ5enO1ODbsVyZYSnEQZQcPXdLY9nXwSFX+pnDw";
let expected_peer_id =
PeerId::from_str("12D3KooWEChVMMMzV8acJ53mJHrw1pQ27UAGkCxWXLJutbeUMvVu").unwrap();
Comment on lines -352 to -354
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this test change? We should not need to change this test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes because of this #3066 (comment). I've been thinking about this and feel uneasy about it. Should we instead use this pattern tafia/quick-protobuf#202 (comment)? That way we're consistent with the official encoding.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we instead use this pattern tafia/quick-protobuf#202 (comment)? That way we're consistent with the official encoding.

I think so yes. We need to be compatible here so this test should not need changing!

PeerId::from_str("16VfNUn8nhtbaQVrQwwBekWYnWNJ4uJRv8pD6T1n1h3CFJr1MMgZM").unwrap();

let encoded = base64::decode(base_64_encoded).unwrap();

Expand Down
7 changes: 0 additions & 7 deletions core/src/identity/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,6 @@ impl DecodingError {
}
}

pub(crate) fn unknown_key_type(key_type: i32) -> Self {
Self {
msg: format!("unknown key-type {key_type}"),
source: None,
}
}
Comment on lines -71 to -76
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉


pub(crate) fn decoding_unsupported(key_type: &'static str) -> Self {
Self {
msg: format!("decoding {key_type} key from Protobuf is unsupported"),
Expand Down
16 changes: 3 additions & 13 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,9 @@

#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]

#[allow(clippy::derive_partial_eq_without_eq)]
mod keys_proto {
include!(concat!(env!("OUT_DIR"), "/keys_proto.rs"));
}
mod protos;

mod envelope_proto {
include!(concat!(env!("OUT_DIR"), "/envelope_proto.rs"));
}

#[allow(clippy::derive_partial_eq_without_eq)]
mod peer_record_proto {
include!(concat!(env!("OUT_DIR"), "/peer_record_proto.rs"));
}
use protos::*;

/// Multi-address re-export.
pub use multiaddr;
Expand Down Expand Up @@ -82,4 +72,4 @@ pub use upgrade::{InboundUpgrade, OutboundUpgrade, ProtocolName, UpgradeError, U

#[derive(thiserror::Error, Debug)]
#[error(transparent)]
pub struct DecodeError(prost::DecodeError);
pub struct DecodeError(quick_protobuf::Error);
38 changes: 14 additions & 24 deletions core/src/peer_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::identity::Keypair;
use crate::signed_envelope::SignedEnvelope;
use crate::{peer_record_proto, signed_envelope, DecodeError, Multiaddr, PeerId};
use instant::SystemTime;
use std::borrow::Cow;
use std::convert::TryInto;

const PAYLOAD_TYPE: &str = "/libp2p/routing-state-record";
Expand All @@ -29,11 +30,11 @@ impl PeerRecord {
///
/// If this function succeeds, the [`SignedEnvelope`] contained a peer record with a valid signature and can hence be considered authenticated.
pub fn from_signed_envelope(envelope: SignedEnvelope) -> Result<Self, FromEnvelopeError> {
use prost::Message;

let (payload, signing_key) =
envelope.payload_and_signing_key(String::from(DOMAIN_SEP), PAYLOAD_TYPE.as_bytes())?;
let record = peer_record_proto::PeerRecord::decode(payload).map_err(DecodeError)?;

let record: peer_record_proto::PeerRecord =
quick_protobuf::deserialize_from_slice(payload).map_err(DecodeError)?;

let peer_id = PeerId::from_bytes(&record.peer_id)?;

Expand All @@ -45,7 +46,7 @@ impl PeerRecord {
let addresses = record
.addresses
.into_iter()
.map(|a| a.multiaddr.try_into())
.map(|a| a.multiaddr.to_vec().try_into())
.collect::<Result<Vec<_>, _>>()?;

Ok(Self {
Expand All @@ -60,8 +61,6 @@ impl PeerRecord {
///
/// This is the same key that is used for authenticating every libp2p connection of your application, i.e. what you use when setting up your [`crate::transport::Transport`].
pub fn new(key: &Keypair, addresses: Vec<Multiaddr>) -> Result<Self, SigningError> {
use prost::Message;

let seq = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("now() is never before UNIX_EPOCH")
Expand All @@ -70,21 +69,17 @@ impl PeerRecord {

let payload = {
let record = peer_record_proto::PeerRecord {
peer_id: peer_id.to_bytes(),
peer_id: Cow::from(peer_id.to_bytes()),
seq,
addresses: addresses
.iter()
.map(|m| peer_record_proto::peer_record::AddressInfo {
multiaddr: m.to_vec(),
.map(|m| peer_record_proto::mod_PeerRecord::AddressInfo {
multiaddr: Cow::Borrowed(m.as_ref()),
})
.collect(),
};

let mut buf = Vec::with_capacity(record.encoded_len());
record
.encode(&mut buf)
.expect("Vec<u8> provides capacity as needed");
buf
quick_protobuf::serialize_into_vec(&record).expect("Encoding to succeed.")
};

let envelope = SignedEnvelope::new(
Expand Down Expand Up @@ -162,8 +157,6 @@ mod tests {

#[test]
fn mismatched_signature() {
use prost::Message;

let addr: Multiaddr = HOME.parse().unwrap();

let envelope = {
Expand All @@ -172,18 +165,15 @@ mod tests {

let payload = {
let record = peer_record_proto::PeerRecord {
peer_id: identity_a.public().to_peer_id().to_bytes(),
peer_id: Cow::from(identity_a.public().to_peer_id().to_bytes()),
seq: 0,
addresses: vec![peer_record_proto::peer_record::AddressInfo {
multiaddr: addr.to_vec(),
addresses: vec![peer_record_proto::mod_PeerRecord::AddressInfo {
multiaddr: Cow::from(addr.to_vec()),
}],
};

let mut buf = Vec::with_capacity(record.encoded_len());
record
.encode(&mut buf)
.expect("Vec<u8> provides capacity as needed");
buf
quick_protobuf::serialize_into_vec(&record)
.expect("Vec<u8> provides capacity as needed")
};

SignedEnvelope::new(
Expand Down
File renamed without changes.
Loading