hotshot_testing/block_builder/
mod.rs

1// Copyright (c) 2021-2024 Espresso Systems (espressosys.com)
2// This file is part of the HotShot repository.
3
4// You should have received a copy of the MIT License
5// along with the HotShot repository. If not, see <https://mit-license.org/>.
6
7use std::collections::HashMap;
8
9use async_broadcast::Receiver;
10use async_trait::async_trait;
11use futures::Stream;
12use hotshot::{traits::BlockPayload, types::Event};
13use hotshot_builder_api::{
14    v0_1::{
15        self,
16        block_info::{AvailableBlockData, AvailableBlockInfo},
17        builder::{Error, Options},
18    },
19    v0_2::block_info::AvailableBlockHeaderInputV1,
20};
21use hotshot_types::{
22    constants::LEGACY_BUILDER_MODULE,
23    traits::{
24        block_contents::EncodeBytes, node_implementation::NodeType,
25        signature_key::BuilderSignatureKey,
26    },
27};
28use tide_disco::{method::ReadState, App, Url};
29use tokio::spawn;
30use vbs::version::StaticVersionType;
31
32use crate::test_builder::BuilderChange;
33
34pub mod random;
35pub use random::RandomBuilderImplementation;
36
37pub mod simple;
38pub use simple::SimpleBuilderImplementation;
39
40#[async_trait]
41pub trait TestBuilderImplementation<TYPES: NodeType>
42where
43    <TYPES as NodeType>::InstanceState: Default,
44{
45    type Config: Default;
46
47    async fn start(
48        num_storage_nodes: usize,
49        url: Url,
50        options: Self::Config,
51        changes: HashMap<u64, BuilderChange>,
52    ) -> Box<dyn BuilderTask<TYPES>>;
53}
54
55pub trait BuilderTask<TYPES: NodeType>: Send + Sync {
56    fn start(
57        self: Box<Self>,
58        stream: Box<dyn Stream<Item = Event<TYPES>> + std::marker::Unpin + Send + 'static>,
59    );
60}
61
62/// Entry for a built block
63#[derive(Debug, Clone)]
64struct BlockEntry<TYPES: NodeType> {
65    metadata: AvailableBlockInfo<TYPES>,
66    payload: Option<AvailableBlockData<TYPES>>,
67    header_input: Option<AvailableBlockHeaderInputV1<TYPES>>,
68}
69
70/// Construct a tide disco app that mocks the builder API 0.1 + 0.3.
71///
72/// # Panics
73/// If constructing and launching the builder fails for any reason
74pub fn run_builder_source<TYPES, Source>(
75    url: Url,
76    mut change_receiver: Receiver<BuilderChange>,
77    source: Source,
78) where
79    TYPES: NodeType,
80    <TYPES as NodeType>::InstanceState: Default,
81    Source: Clone + Send + Sync + tide_disco::method::ReadState + 'static,
82    <Source as ReadState>::State: Sync + Send + v0_1::data_source::BuilderDataSource<TYPES>,
83{
84    spawn(async move {
85        let start_builder = |url: Url, source: Source| -> _ {
86            let builder_api_0_1 = hotshot_builder_api::v0_1::builder::define_api::<Source, TYPES>(
87                &Options::default(),
88            )
89            .expect("Failed to construct the builder API");
90
91            let mut app: App<Source, Error> = App::with_state(source);
92            app.register_module(LEGACY_BUILDER_MODULE, builder_api_0_1)
93                .expect("Failed to register the builder API 0.1");
94            spawn(app.serve(url, hotshot_builder_api::v0_1::Version::instance()))
95        };
96
97        let mut handle = Some(start_builder(url.clone(), source.clone()));
98
99        while let Ok(event) = change_receiver.recv().await {
100            match event {
101                BuilderChange::Up if handle.is_none() => {
102                    handle = Some(start_builder(url.clone(), source.clone()));
103                },
104                BuilderChange::Down => {
105                    if let Some(handle) = handle.take() {
106                        handle.abort();
107                    }
108                },
109                _ => {},
110            }
111        }
112    });
113}
114
115/// Construct a tide disco app that mocks the builder API 0.1.
116///
117/// # Panics
118/// If constructing and launching the builder fails for any reason
119pub fn run_builder_source_0_1<TYPES, Source>(
120    url: Url,
121    mut change_receiver: Receiver<BuilderChange>,
122    source: Source,
123) where
124    TYPES: NodeType,
125    <TYPES as NodeType>::InstanceState: Default,
126    Source: Clone + Send + Sync + tide_disco::method::ReadState + 'static,
127    <Source as ReadState>::State: Sync + Send + v0_1::data_source::BuilderDataSource<TYPES>,
128{
129    spawn(async move {
130        let start_builder = |url: Url, source: Source| -> _ {
131            let builder_api = hotshot_builder_api::v0_1::builder::define_api::<Source, TYPES>(
132                &Options::default(),
133            )
134            .expect("Failed to construct the builder API");
135            let mut app: App<Source, Error> = App::with_state(source);
136            app.register_module(LEGACY_BUILDER_MODULE, builder_api)
137                .expect("Failed to register the builder API");
138            spawn(app.serve(url, hotshot_builder_api::v0_1::Version::instance()))
139        };
140
141        let mut handle = Some(start_builder(url.clone(), source.clone()));
142
143        while let Ok(event) = change_receiver.recv().await {
144            match event {
145                BuilderChange::Up if handle.is_none() => {
146                    handle = Some(start_builder(url.clone(), source.clone()));
147                },
148                BuilderChange::Down => {
149                    if let Some(handle) = handle.take() {
150                        handle.abort();
151                    }
152                },
153                _ => {},
154            }
155        }
156    });
157}
158
159/// Helper function to construct all builder data structures from a list of transactions
160async fn build_block<TYPES: NodeType>(
161    transactions: Vec<TYPES::Transaction>,
162    pub_key: TYPES::BuilderSignatureKey,
163    priv_key: <TYPES::BuilderSignatureKey as BuilderSignatureKey>::BuilderPrivateKey,
164) -> BlockEntry<TYPES>
165where
166    <TYPES as NodeType>::InstanceState: Default,
167{
168    let (block_payload, metadata) = TYPES::BlockPayload::from_transactions(
169        transactions,
170        &Default::default(),
171        &Default::default(),
172    )
173    .await
174    .expect("failed to build block payload from transactions");
175
176    let commitment = block_payload.builder_commitment(&metadata);
177
178    // Get block size from the encoded payload
179    let block_size = block_payload.encode().len() as u64;
180
181    let signature_over_block_info =
182        TYPES::BuilderSignatureKey::sign_block_info(&priv_key, block_size, 123, &commitment)
183            .expect("Failed to sign block info");
184
185    let signature_over_builder_commitment =
186        TYPES::BuilderSignatureKey::sign_builder_message(&priv_key, commitment.as_ref())
187            .expect("Failed to sign commitment");
188
189    let signature_over_fee_info =
190        TYPES::BuilderSignatureKey::sign_fee(&priv_key, 123_u64, &metadata)
191            .expect("Failed to sign fee info");
192
193    let block = AvailableBlockData {
194        block_payload,
195        metadata,
196        sender: pub_key.clone(),
197        signature: signature_over_builder_commitment,
198    };
199    let metadata = AvailableBlockInfo {
200        sender: pub_key.clone(),
201        signature: signature_over_block_info,
202        block_hash: commitment,
203        block_size,
204        offered_fee: 123,
205        _phantom: std::marker::PhantomData,
206    };
207    let header_input = AvailableBlockHeaderInputV1 {
208        fee_signature: signature_over_fee_info,
209        sender: pub_key,
210    };
211
212    BlockEntry {
213        metadata,
214        payload: Some(block),
215        header_input: Some(header_input),
216    }
217}