Skip to content

Commit 29efac0

Browse files
authored
P2P Flashblocks Propagation (#373)
feat: P2P Flashblocks Propagation
1 parent 6da64fc commit 29efac0

File tree

23 files changed

+3738
-698
lines changed

23 files changed

+3738
-698
lines changed

Cargo.lock

Lines changed: 2408 additions & 564 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@ resolver = "3"
44
members = [
55
"crates/rollup-boost",
66
"crates/websocket-proxy",
7+
"crates/rollup-boost-types",
78
]
89

910
[workspace.dependencies]
11+
rollup-boost = { path = "crates/rollup-boost" }
12+
flashblocks-rpc = { path = "crates/flashblocks-rpc" }
13+
rollup-boost-types = { path = "crates/rollup-boost-types" }
14+
1015
backoff = "0.4.0"
1116
clap = { version = "4", features = ["derive", "env"] }
1217
eyre = "0.6.12"
@@ -22,6 +27,13 @@ tokio = { version = "1", features = ["full"] }
2227
tracing = "0.1.4"
2328
tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] }
2429
url = "2.2.0"
30+
ed25519-dalek = { version = "2", features = ["serde"] }
31+
blake3 = "1"
32+
hex = "0.4"
33+
34+
# Reth deps, use 4231f4b to get latest op-alloy
35+
reth-optimism-payload-builder = { git = "https://github.com/paradigmxyz/reth", rev = "4231f4b" }
36+
reth-rpc-layer = { git = "https://github.com/paradigmxyz/reth", rev = "4231f4b" }
2537

2638
# Alloy libraries
2739
alloy-rpc-types-engine = "1.0.41"
@@ -34,12 +46,14 @@ alloy-consensus = "1.0.41"
3446
alloy-rpc-types = "1.0.41"
3547
alloy-genesis = "1.0.41"
3648
alloy-rpc-client = "1.0.41"
49+
alloy-rlp = "0.3.12"
3750
alloy-provider = "1.0.41"
3851
op-alloy-network = "0.23.0"
39-
op-alloy-rpc-types-engine = "0.23.0"
40-
op-alloy-consensus = "0.23.0"
41-
op-alloy-rpc-types = "0.23.0"
52+
op-alloy-rpc-types-engine = "0.23.1"
53+
op-alloy-consensus = "0.23.1"
54+
op-alloy-rpc-types = "0.23.1"
4255
tokio-tungstenite = { version = "0.26.2", features = ["native-tls"] }
4356
testcontainers = "0.23"
4457
jsonrpsee = "0.26.0"
4558
testcontainers-modules = { version = "0.11", features = ["redis"] }
59+
redis-test = "1"

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ RUN cargo install sccache --version ^0.9
1515
RUN cargo install cargo-chef --version ^0.1
1616

1717
RUN apt-get update \
18-
&& apt-get install -y clang libclang-dev
18+
&& apt-get install -y clang libclang-dev gcc
1919

2020
ENV CARGO_HOME=/usr/local/cargo
2121
ENV RUSTC_WRAPPER=sccache
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "rollup-boost-types"
3+
version = "0.1.0"
4+
edition = "2024"
5+
license = "MIT"
6+
7+
[dependencies]
8+
alloy-primitives = { workspace = true }
9+
alloy-rlp = { workspace = true }
10+
alloy-rpc-types-engine = { workspace = true }
11+
alloy-rpc-types-eth = { workspace = true }
12+
alloy-serde = { workspace = true }
13+
futures = { workspace = true }
14+
moka = { workspace = true }
15+
op-alloy-rpc-types-engine = { workspace = true }
16+
serde = { workspace = true }
17+
serde_json = { workspace = true }
18+
tracing = { workspace = true }
19+
thiserror = { workspace = true }
20+
ed25519-dalek = { workspace = true }
21+
blake3 = { workspace = true }
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
use alloy_primitives::{B64, Bytes};
2+
use alloy_rlp::{Decodable, Encodable, Header};
3+
use alloy_rpc_types_engine::PayloadId;
4+
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
5+
use serde::{Deserialize, Serialize};
6+
use thiserror::Error;
7+
8+
/// An authorization token that grants a builder permission to publish flashblocks for a specific payload.
9+
///
10+
/// The `authorizer_sig` is made over the `payload_id`, `timestamp`, and `builder_vk`. This is
11+
/// useful because it allows the authorizer to control which builders can publish flashblocks in
12+
/// real time, without relying on consumers to verify the builder's public key against a
13+
/// pre-defined list.
14+
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
15+
pub struct Authorization {
16+
/// The unique identifier of the payload this authorization applies to
17+
pub payload_id: PayloadId,
18+
/// Unix timestamp when this authorization was created
19+
pub timestamp: u64,
20+
/// The public key of the builder who is authorized to sign messages
21+
pub builder_vk: VerifyingKey,
22+
/// The authorizer's signature over the payload_id, timestamp, and builder_vk
23+
pub authorizer_sig: Signature,
24+
}
25+
26+
#[derive(Debug, Error, PartialEq)]
27+
pub enum AuthorizationError {
28+
#[error("invalid authorizer signature")]
29+
InvalidAuthorizerSig,
30+
}
31+
32+
impl Authorization {
33+
/// Creates a new authorization token for a builder to publish messages for a specific payload.
34+
///
35+
/// This function creates a cryptographic authorization by signing a message containing the
36+
/// payload ID, timestamp, and builder's public key using the authorizer's signing key.
37+
///
38+
/// # Arguments
39+
///
40+
/// * `payload_id` - The unique identifier of the payload this authorization applies to
41+
/// * `timestamp` - Unix timestamp associated with this `payload_id`
42+
/// * `authorizer_sk` - The authorizer's signing key used to create the signature
43+
/// * `actor_vk` - The verifying key of the actor being authorized
44+
///
45+
/// # Returns
46+
///
47+
/// A new `Authorization` instance with the generated signature
48+
pub fn new(
49+
payload_id: PayloadId,
50+
timestamp: u64,
51+
authorizer_sk: &SigningKey,
52+
actor_vk: VerifyingKey,
53+
) -> Self {
54+
let mut msg = payload_id.0.to_vec();
55+
msg.extend_from_slice(&timestamp.to_le_bytes());
56+
msg.extend_from_slice(actor_vk.as_bytes());
57+
let hash = blake3::hash(&msg);
58+
let sig = authorizer_sk.sign(hash.as_bytes());
59+
60+
Self {
61+
payload_id,
62+
timestamp,
63+
builder_vk: actor_vk,
64+
authorizer_sig: sig,
65+
}
66+
}
67+
68+
/// Verifies the authorization signature against the provided authorizer's verifying key.
69+
///
70+
/// This function reconstructs the signed message from the authorization data and verifies
71+
/// that the signature was created by the holder of the authorizer's private key.
72+
///
73+
/// # Arguments
74+
///
75+
/// * `authorizer_sk` - The verifying key of the authorizer to verify against
76+
///
77+
/// # Returns
78+
///
79+
/// * `Ok(())` if the signature is valid
80+
/// * `Err(FlashblocksP2PError::InvalidAuthorizerSig)` if the signature is invalid
81+
pub fn verify(&self, authorizer_sk: VerifyingKey) -> Result<(), AuthorizationError> {
82+
let mut msg = self.payload_id.0.to_vec();
83+
msg.extend_from_slice(&self.timestamp.to_le_bytes());
84+
msg.extend_from_slice(self.builder_vk.as_bytes());
85+
let hash = blake3::hash(&msg);
86+
authorizer_sk
87+
.verify(hash.as_bytes(), &self.authorizer_sig)
88+
.map_err(|_| AuthorizationError::InvalidAuthorizerSig)
89+
}
90+
}
91+
92+
impl Encodable for Authorization {
93+
fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
94+
// pre-serialize the key & sig once so we can reuse the bytes & lengths
95+
let pub_bytes = Bytes::copy_from_slice(self.builder_vk.as_bytes()); // 33 bytes
96+
let sig_bytes = Bytes::copy_from_slice(&self.authorizer_sig.to_bytes()); // 64 bytes
97+
98+
let payload_len = self.payload_id.0.length()
99+
+ self.timestamp.length()
100+
+ pub_bytes.length()
101+
+ sig_bytes.length();
102+
103+
Header {
104+
list: true,
105+
payload_length: payload_len,
106+
}
107+
.encode(out);
108+
109+
// 1. payload_id (inner B64 already Encodable)
110+
self.payload_id.0.encode(out);
111+
// 2. timestamp
112+
self.timestamp.encode(out);
113+
// 3. builder_pub
114+
pub_bytes.encode(out);
115+
// 4. authorizer_sig
116+
sig_bytes.encode(out);
117+
}
118+
119+
fn length(&self) -> usize {
120+
let pub_bytes = Bytes::copy_from_slice(self.builder_vk.as_bytes());
121+
let sig_bytes = Bytes::copy_from_slice(&self.authorizer_sig.to_bytes());
122+
123+
let payload_len = self.payload_id.0.length()
124+
+ self.timestamp.length()
125+
+ pub_bytes.length()
126+
+ sig_bytes.length();
127+
128+
Header {
129+
list: true,
130+
payload_length: payload_len,
131+
}
132+
.length()
133+
+ payload_len
134+
}
135+
}
136+
137+
impl Decodable for Authorization {
138+
fn decode(buf: &mut &[u8]) -> Result<Self, alloy_rlp::Error> {
139+
let header = Header::decode(buf)?;
140+
if !header.list {
141+
return Err(alloy_rlp::Error::UnexpectedString);
142+
}
143+
let mut body = &buf[..header.payload_length];
144+
145+
// 1. payload_id
146+
let payload_id = alloy_rpc_types_engine::PayloadId(B64::decode(&mut body)?);
147+
148+
// 2. timestamp
149+
let timestamp = u64::decode(&mut body)?;
150+
151+
// 3. builder_pub
152+
let pub_bytes = Bytes::decode(&mut body)?;
153+
let builder_pub = VerifyingKey::try_from(pub_bytes.as_ref())
154+
.map_err(|_| alloy_rlp::Error::Custom("bad builder_pub"))?;
155+
156+
// 4. authorizer_sig
157+
let sig_bytes = Bytes::decode(&mut body)?;
158+
let authorizer_sig = Signature::try_from(sig_bytes.as_ref())
159+
.map_err(|_| alloy_rlp::Error::Custom("bad signature"))?;
160+
161+
// advance caller’s slice cursor
162+
*buf = &buf[header.payload_length..];
163+
164+
Ok(Self {
165+
payload_id,
166+
timestamp,
167+
builder_vk: builder_pub,
168+
authorizer_sig,
169+
})
170+
}
171+
}
172+
173+
#[cfg(test)]
174+
mod tests {
175+
use super::*;
176+
use alloy_rlp::{Decodable, Encodable, encode};
177+
178+
fn key_pair(seed: u8) -> (SigningKey, VerifyingKey) {
179+
let bytes = [seed; 32];
180+
let sk = SigningKey::from_bytes(&bytes);
181+
let vk = sk.verifying_key();
182+
(sk, vk)
183+
}
184+
185+
#[test]
186+
fn authorization_rlp_roundtrip_and_verify() {
187+
let (authorizer_sk, authorizer_vk) = key_pair(1);
188+
let (_, builder_vk) = key_pair(2);
189+
190+
let auth = Authorization::new(
191+
PayloadId::default(),
192+
1_700_000_123,
193+
&authorizer_sk,
194+
builder_vk,
195+
);
196+
197+
let encoded = encode(auth);
198+
assert_eq!(encoded.len(), auth.length(), "length impl correct");
199+
200+
let mut slice = encoded.as_ref();
201+
let decoded = Authorization::decode(&mut slice).expect("decoding succeeds");
202+
assert!(slice.is_empty(), "decoder consumed all bytes");
203+
assert_eq!(decoded, auth, "round-trip preserves value");
204+
205+
// Signature is valid
206+
decoded.verify(authorizer_vk).expect("signature verifies");
207+
}
208+
209+
#[test]
210+
fn authorization_signature_tamper_is_detected() {
211+
let (authorizer_sk, authorizer_vk) = key_pair(1);
212+
let (_, builder_vk) = key_pair(2);
213+
214+
let mut auth = Authorization::new(PayloadId::default(), 42, &authorizer_sk, builder_vk);
215+
216+
let mut sig_bytes = auth.authorizer_sig.to_bytes();
217+
sig_bytes[0] ^= 1;
218+
auth.authorizer_sig = Signature::try_from(sig_bytes.as_ref()).unwrap();
219+
220+
assert!(auth.verify(authorizer_vk).is_err());
221+
}
222+
}

0 commit comments

Comments
 (0)