Skip to content

Commit 1173cf6

Browse files
authored
Move response logic out of runway (#496)
1 parent 6ef434f commit 1173cf6

File tree

12 files changed

+487
-155
lines changed

12 files changed

+487
-155
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

consensus/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "aleph-bft"
3-
version = "0.38.1"
3+
version = "0.38.2"
44
edition = "2021"
55
authors = ["Cardinal Cryptography"]
66
categories = ["algorithms", "data-structures", "cryptography", "database"]

consensus/src/dissemination/mod.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use crate::{
2+
runway::{NewestUnitResponse, Salt},
3+
units::{UncheckedSignedUnit, UnitCoord},
4+
Data, Hasher, NodeIndex, Signature, UncheckedSigned,
5+
};
6+
7+
mod responder;
8+
9+
pub use responder::Responder;
10+
11+
/// Possible requests for information from other nodes.
12+
#[derive(Debug)]
13+
pub enum Request<H: Hasher> {
14+
Coord(UnitCoord),
15+
Parents(H::Hash),
16+
NewestUnit(NodeIndex, Salt),
17+
}
18+
19+
/// Responses to requests.
20+
#[derive(Debug)]
21+
pub enum Response<H: Hasher, D: Data, S: Signature> {
22+
Coord(UncheckedSignedUnit<H, D, S>),
23+
Parents(H::Hash, Vec<UncheckedSignedUnit<H, D, S>>),
24+
NewestUnit(UncheckedSigned<NewestUnitResponse<H, D, S>, S>),
25+
}
Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
use crate::{
2+
dag::DagUnit,
3+
dissemination::{Request, Response},
4+
runway::{NewestUnitResponse, Salt},
5+
units::{UnitCoord, UnitStore, UnitWithParents, WrappedUnit},
6+
Data, Hasher, MultiKeychain, NodeIndex, Signed,
7+
};
8+
use std::marker::PhantomData;
9+
use thiserror::Error;
10+
11+
/// A responder that is able to answer requests for data about units.
12+
pub struct Responder<H: Hasher, D: Data, MK: MultiKeychain> {
13+
keychain: MK,
14+
_phantom: PhantomData<(H, D)>,
15+
}
16+
17+
/// Ways in which it can be impossible for us to respond to a request.
18+
#[derive(Eq, Error, Debug, PartialEq)]
19+
pub enum Error<H: Hasher> {
20+
#[error("no canonical unit at {0}")]
21+
NoCanonicalAt(UnitCoord),
22+
#[error("unit with hash {0:?} not known")]
23+
UnknownUnit(H::Hash),
24+
}
25+
26+
impl<H: Hasher, D: Data, MK: MultiKeychain> Responder<H, D, MK> {
27+
/// Create a new responder.
28+
pub fn new(keychain: MK) -> Self {
29+
Responder {
30+
keychain,
31+
_phantom: PhantomData,
32+
}
33+
}
34+
35+
fn index(&self) -> NodeIndex {
36+
self.keychain.index()
37+
}
38+
39+
fn on_request_coord(
40+
&self,
41+
coord: UnitCoord,
42+
units: &UnitStore<DagUnit<H, D, MK>>,
43+
) -> Result<Response<H, D, MK::Signature>, Error<H>> {
44+
units
45+
.canonical_unit(coord)
46+
.map(|unit| Response::Coord(unit.clone().unpack().into()))
47+
.ok_or(Error::NoCanonicalAt(coord))
48+
}
49+
50+
fn on_request_parents(
51+
&self,
52+
hash: H::Hash,
53+
units: &UnitStore<DagUnit<H, D, MK>>,
54+
) -> Result<Response<H, D, MK::Signature>, Error<H>> {
55+
units
56+
.unit(&hash)
57+
.map(|unit| {
58+
let parents = unit
59+
.parents()
60+
.values()
61+
.map(|parent_hash| {
62+
units
63+
.unit(parent_hash)
64+
.expect("Units are added to the store in order.")
65+
.clone()
66+
.unpack()
67+
.into_unchecked()
68+
})
69+
.collect();
70+
Response::Parents(hash, parents)
71+
})
72+
.ok_or(Error::UnknownUnit(hash))
73+
}
74+
75+
fn on_request_newest(
76+
&self,
77+
requester: NodeIndex,
78+
salt: Salt,
79+
units: &UnitStore<DagUnit<H, D, MK>>,
80+
) -> Response<H, D, MK::Signature> {
81+
let unit = units
82+
.canonical_units(requester)
83+
.last()
84+
.map(|unit| unit.clone().unpack().into_unchecked());
85+
let response = NewestUnitResponse::new(requester, self.index(), unit, salt);
86+
87+
let signed_response = Signed::sign(response, &self.keychain).into_unchecked();
88+
Response::NewestUnit(signed_response)
89+
}
90+
91+
/// Handle an incoming request returning either the appropriate response or an error if we
92+
/// aren't able to help.
93+
pub fn handle_request(
94+
&self,
95+
request: Request<H>,
96+
units: &UnitStore<DagUnit<H, D, MK>>,
97+
) -> Result<Response<H, D, MK::Signature>, Error<H>> {
98+
use Request::*;
99+
match request {
100+
Coord(coord) => self.on_request_coord(coord, units),
101+
Parents(hash) => self.on_request_parents(hash, units),
102+
NewestUnit(node_id, salt) => Ok(self.on_request_newest(node_id, salt, units)),
103+
}
104+
}
105+
}
106+
107+
#[cfg(test)]
108+
mod test {
109+
use crate::{
110+
dissemination::{
111+
responder::{Error, Responder},
112+
Request, Response,
113+
},
114+
units::{
115+
random_full_parent_reconstrusted_units_up_to, TestingDagUnit, Unit, UnitCoord,
116+
UnitStore, UnitWithParents, WrappedUnit,
117+
},
118+
NodeCount, NodeIndex,
119+
};
120+
use aleph_bft_mock::{Data, Hasher64, Keychain};
121+
use std::iter::zip;
122+
123+
const NODE_ID: NodeIndex = NodeIndex(0);
124+
const NODE_COUNT: NodeCount = NodeCount(7);
125+
126+
fn setup() -> (
127+
Responder<Hasher64, Data, Keychain>,
128+
UnitStore<TestingDagUnit>,
129+
Vec<Keychain>,
130+
) {
131+
let keychains = Keychain::new_vec(NODE_COUNT);
132+
(
133+
Responder::new(keychains[NODE_ID.0]),
134+
UnitStore::new(NODE_COUNT),
135+
keychains,
136+
)
137+
}
138+
139+
#[test]
140+
fn empty_fails_to_respond_to_coords() {
141+
let (responder, store, _) = setup();
142+
let coord = UnitCoord::new(0, NodeIndex(1));
143+
let request = Request::Coord(coord);
144+
match responder.handle_request(request, &store) {
145+
Ok(response) => panic!("Unexpected response: {:?}.", response),
146+
Err(err) => assert_eq!(err, Error::NoCanonicalAt(coord)),
147+
}
148+
}
149+
150+
#[test]
151+
fn empty_fails_to_respond_to_parents() {
152+
let (responder, store, keychains) = setup();
153+
let session_id = 2137;
154+
let hash =
155+
random_full_parent_reconstrusted_units_up_to(1, NODE_COUNT, session_id, &keychains)
156+
.last()
157+
.expect("just created this round")
158+
.last()
159+
.expect("the round has at least one unit")
160+
.hash();
161+
let request = Request::Parents(hash);
162+
match responder.handle_request(request, &store) {
163+
Ok(response) => panic!("Unexpected response: {:?}.", response),
164+
Err(err) => assert_eq!(err, Error::UnknownUnit(hash)),
165+
}
166+
}
167+
168+
#[test]
169+
fn empty_newest_responds_with_no_units() {
170+
let (responder, store, keychains) = setup();
171+
let requester = NodeIndex(1);
172+
let request = Request::NewestUnit(requester, rand::random());
173+
let response = responder
174+
.handle_request(request, &store)
175+
.expect("newest unit requests always get a response");
176+
match response {
177+
Response::NewestUnit(newest_unit_response) => {
178+
let checked_newest_unit_response = newest_unit_response
179+
.check(&keychains[NODE_ID.0])
180+
.expect("should sign correctly");
181+
assert_eq!(
182+
checked_newest_unit_response.as_signable().requester(),
183+
requester
184+
);
185+
assert!(checked_newest_unit_response
186+
.as_signable()
187+
.included_data()
188+
.is_empty());
189+
}
190+
other => panic!("Unexpected response: {:?}.", other),
191+
}
192+
}
193+
194+
#[test]
195+
fn responds_to_coords_when_possible() {
196+
let (responder, mut store, keychains) = setup();
197+
let session_id = 2137;
198+
let coord = UnitCoord::new(3, NodeIndex(1));
199+
let units = random_full_parent_reconstrusted_units_up_to(
200+
coord.round() + 1,
201+
NODE_COUNT,
202+
session_id,
203+
&keychains,
204+
);
205+
for round_units in &units {
206+
for unit in round_units {
207+
store.insert(unit.clone());
208+
}
209+
}
210+
let request = Request::Coord(coord);
211+
let response = responder
212+
.handle_request(request, &store)
213+
.expect("should successfully respond");
214+
match response {
215+
Response::Coord(unit) => assert_eq!(
216+
unit,
217+
units[coord.round() as usize][coord.creator().0]
218+
.clone()
219+
.unpack()
220+
.into_unchecked()
221+
),
222+
other => panic!("Unexpected response: {:?}.", other),
223+
}
224+
}
225+
226+
#[test]
227+
fn fails_to_responds_to_too_new_coords() {
228+
let (responder, mut store, keychains) = setup();
229+
let session_id = 2137;
230+
let coord = UnitCoord::new(3, NodeIndex(1));
231+
let units = random_full_parent_reconstrusted_units_up_to(
232+
coord.round() - 1,
233+
NODE_COUNT,
234+
session_id,
235+
&keychains,
236+
);
237+
for round_units in &units {
238+
for unit in round_units {
239+
store.insert(unit.clone());
240+
}
241+
}
242+
let request = Request::Coord(coord);
243+
match responder.handle_request(request, &store) {
244+
Ok(response) => panic!("Unexpected response: {:?}.", response),
245+
Err(err) => assert_eq!(err, Error::NoCanonicalAt(coord)),
246+
}
247+
}
248+
249+
#[test]
250+
fn responds_to_parents_when_possible() {
251+
let (responder, mut store, keychains) = setup();
252+
let session_id = 2137;
253+
let units =
254+
random_full_parent_reconstrusted_units_up_to(5, NODE_COUNT, session_id, &keychains);
255+
for round_units in &units {
256+
for unit in round_units {
257+
store.insert(unit.clone());
258+
}
259+
}
260+
let requested_unit = units
261+
.last()
262+
.expect("just created this round")
263+
.last()
264+
.expect("the round has at least one unit")
265+
.clone();
266+
let request = Request::Parents(requested_unit.hash());
267+
let response = responder
268+
.handle_request(request, &store)
269+
.expect("should successfully respond");
270+
match response {
271+
Response::Parents(response_hash, parents) => {
272+
assert_eq!(response_hash, requested_unit.hash());
273+
assert_eq!(parents.len(), requested_unit.parents().size().0);
274+
for (parent, parent_hash) in zip(parents, requested_unit.parents().values()) {
275+
assert_eq!(&parent.as_signable().hash(), parent_hash);
276+
}
277+
}
278+
other => panic!("Unexpected response: {:?}.", other),
279+
}
280+
}
281+
282+
#[test]
283+
fn fails_to_respond_to_unknown_parents() {
284+
let (responder, mut store, keychains) = setup();
285+
let session_id = 2137;
286+
let units =
287+
random_full_parent_reconstrusted_units_up_to(5, NODE_COUNT, session_id, &keychains);
288+
for round_units in &units {
289+
for unit in round_units {
290+
store.insert(unit.clone());
291+
}
292+
}
293+
let hash =
294+
random_full_parent_reconstrusted_units_up_to(1, NODE_COUNT, session_id, &keychains)
295+
.last()
296+
.expect("just created this round")
297+
.last()
298+
.expect("the round has at least one unit")
299+
.hash();
300+
let request = Request::Parents(hash);
301+
match responder.handle_request(request, &store) {
302+
Ok(response) => panic!("Unexpected response: {:?}.", response),
303+
Err(err) => assert_eq!(err, Error::UnknownUnit(hash)),
304+
}
305+
}
306+
307+
#[test]
308+
fn responds_to_existing_newest() {
309+
let (responder, mut store, keychains) = setup();
310+
let session_id = 2137;
311+
let units =
312+
random_full_parent_reconstrusted_units_up_to(5, NODE_COUNT, session_id, &keychains);
313+
for round_units in &units {
314+
for unit in round_units {
315+
store.insert(unit.clone());
316+
}
317+
}
318+
let requester = NodeIndex(1);
319+
let request = Request::NewestUnit(requester, rand::random());
320+
let response = responder
321+
.handle_request(request, &store)
322+
.expect("newest unit requests always get a response");
323+
match response {
324+
Response::NewestUnit(newest_unit_response) => {
325+
let checked_newest_unit_response = newest_unit_response
326+
.check(&keychains[NODE_ID.0])
327+
.expect("should sign correctly");
328+
assert_eq!(
329+
checked_newest_unit_response.as_signable().requester(),
330+
requester
331+
);
332+
// unfortunately there is no easy way to check whether the response contains a unit
333+
// with its API :/
334+
}
335+
other => panic!("Unexpected response: {:?}.", other),
336+
}
337+
}
338+
}

0 commit comments

Comments
 (0)