psibase/
boot.rs

1use crate::services::{accounts, auth_delegate, auth_sig, producers, transact};
2use crate::{
3    method_raw, new_account_action, set_auth_service_action, set_key_action, validate_dependencies,
4    AccountNumber, Action, AnyPublicKey, Claim, ExactAccountNumber, GenesisActionData,
5    MethodNumber, PackagedService, Producer, SignedTransaction, Tapos, TimePointSec, Transaction,
6};
7use fracpack::Pack;
8use serde_bytes::ByteBuf;
9use sha2::{Digest, Sha256};
10use std::collections::HashSet;
11use std::io::{Cursor, Read, Seek};
12use std::str::FromStr;
13use wasm_bindgen::prelude::*;
14
15macro_rules! method {
16    ($name:expr) => {
17        MethodNumber::new(method_raw!($name))
18    };
19}
20
21fn set_producers_action(name: AccountNumber, key: Claim) -> Action {
22    producers::Wrapper::pack().setProducers(vec![Producer {
23        name: name,
24        auth: key,
25    }])
26}
27
28fn to_claim(key: &AnyPublicKey) -> Claim {
29    Claim {
30        service: key.key.service,
31        rawData: key.key.rawData.clone(),
32    }
33}
34
35fn without_tapos(actions: Vec<Action>, expiration: TimePointSec) -> Transaction {
36    Transaction {
37        tapos: Tapos {
38            expiration,
39            refBlockSuffix: 0,
40            flags: 0,
41            refBlockIndex: 0,
42        },
43        actions,
44        claims: vec![],
45    }
46}
47
48fn genesis_transaction<R: Read + Seek>(
49    expiration: TimePointSec,
50    service_packages: &mut [PackagedService<R>],
51) -> Result<SignedTransaction, anyhow::Error> {
52    let mut services = vec![];
53    for s in service_packages {
54        s.get_genesis(&mut services)?
55    }
56
57    let genesis_action_data = GenesisActionData {
58        memo: "".to_string(),
59        services,
60    };
61
62    let actions = vec![Action {
63        // TODO: set sender,service,method in a way that's helpful to block explorers
64        sender: AccountNumber { value: 0 },
65        service: AccountNumber { value: 0 },
66        method: method!("boot"),
67        rawData: genesis_action_data.packed().into(),
68    }];
69
70    Ok(SignedTransaction {
71        transaction: without_tapos(actions, expiration).packed().into(),
72        proofs: vec![],
73    })
74}
75
76/// Get initial actions
77///
78/// This returns all actions that need to be packed into the transactions pushed after the
79/// boot block.
80pub fn get_initial_actions<R: Read + Seek>(
81    initial_key: &Option<AnyPublicKey>,
82    initial_producer: AccountNumber,
83    install_ui: bool,
84    service_packages: &mut [PackagedService<R>],
85    compression_level: u32,
86) -> Result<Vec<Action>, anyhow::Error> {
87    let mut actions = Vec::new();
88    let has_packages = true;
89
90    for s in &mut service_packages[..] {
91        for account in s.get_accounts() {
92            if !s.has_service(*account) {
93                actions.push(new_account_action(accounts::SERVICE, *account))
94            }
95        }
96
97        if install_ui {
98            s.reg_server(&mut actions)?;
99            s.store_data(&mut actions, compression_level)?;
100        }
101
102        s.postinstall(&mut actions)?;
103    }
104
105    // Create producer account
106    actions.push(new_account_action(accounts::SERVICE, initial_producer));
107
108    let mut claim = Claim {
109        service: AccountNumber::new(0),
110        rawData: Default::default(),
111    };
112    if let Some(key) = initial_key {
113        // Set transaction signing key for producer
114        actions.push(set_key_action(initial_producer, &key));
115        actions.push(set_auth_service_action(initial_producer, auth_sig::SERVICE));
116        claim = to_claim(&key);
117    }
118
119    // Set the producers
120    actions.push(set_producers_action(initial_producer, claim));
121
122    actions.push(new_account_action(accounts::SERVICE, producers::ROOT));
123    actions.push(
124        auth_delegate::Wrapper::pack_from(producers::ROOT)
125            .setOwner(producers::PRODUCER_ACCOUNT_STRONG),
126    );
127    actions.push(set_auth_service_action(
128        producers::ROOT,
129        auth_delegate::SERVICE,
130    ));
131
132    // If a package sets an auth service for an account, we should not override it
133    let mut accounts_with_auth = HashSet::new();
134    for act in &actions {
135        if act.service == accounts::SERVICE && act.method == method!("setAuthServ") {
136            accounts_with_auth.insert(act.sender);
137        }
138    }
139
140    for s in &service_packages[..] {
141        for account in s.get_accounts() {
142            if !accounts_with_auth.contains(account) {
143                actions.push(auth_delegate::Wrapper::pack_from(*account).setOwner(producers::ROOT));
144                actions.push(set_auth_service_action(*account, auth_delegate::SERVICE));
145            }
146        }
147    }
148
149    if has_packages {
150        for s in &mut service_packages[..] {
151            s.commit_install(producers::ROOT, &mut actions)?;
152        }
153    }
154
155    actions.push(transact::Wrapper::pack().finishBoot());
156
157    Ok(actions)
158}
159
160/// Create boot transactions
161///
162/// This returns two sets of transactions which boot a blockchain.
163/// The first set MUST be pushed as a group using push_boot and
164/// will be included in the first block. The remaining transactions
165/// MUST be pushed in order, but are not required to be in the first
166/// block. If any of these transactions fail, the chain will be unusable.
167///
168/// The first transaction, the genesis transaction, installs
169/// a set of service WASMs. The remainder initialize the services
170/// and install apps and documentation.
171///
172/// If `initial_key` is set, then this initializes all accounts to use
173/// that key and sets the key the initial producer signs blocks with.
174/// If it is not set, then this initializes all accounts to use
175/// `auth-any` (no keys required) and sets it up so producers
176/// don't need to sign blocks.
177pub fn create_boot_transactions<R: Read + Seek>(
178    initial_key: &Option<AnyPublicKey>,
179    initial_producer: AccountNumber,
180    install_ui: bool,
181    expiration: TimePointSec,
182    service_packages: &mut [PackagedService<R>],
183    compression_level: u32,
184) -> Result<(Vec<SignedTransaction>, Vec<SignedTransaction>), anyhow::Error> {
185    validate_dependencies(service_packages)?;
186    let mut boot_transactions = vec![genesis_transaction(expiration, service_packages)?];
187    let mut actions = get_initial_actions(
188        initial_key,
189        initial_producer,
190        install_ui,
191        service_packages,
192        compression_level,
193    )?;
194    let mut transactions = Vec::new();
195    while !actions.is_empty() {
196        let mut n = 0;
197        let mut size = 0;
198        while n < actions.len() && size < 1024 * 1024 {
199            size += actions[n].rawData.len();
200            n += 1;
201        }
202        transactions.push(SignedTransaction {
203            transaction: without_tapos(actions.drain(..n).collect(), expiration)
204                .packed()
205                .into(),
206            proofs: vec![],
207        });
208    }
209
210    let mut transaction_ids: Vec<crate::Checksum256> = Vec::new();
211    for trx in &transactions {
212        transaction_ids.push(crate::Checksum256::from(<[u8; 32]>::from(Sha256::digest(
213            &trx.transaction,
214        ))))
215    }
216    boot_transactions.push(SignedTransaction {
217        transaction: without_tapos(
218            vec![transact::Wrapper::pack().startBoot(transaction_ids)],
219            expiration,
220        )
221        .packed()
222        .into(),
223        proofs: vec![],
224    });
225    Ok((boot_transactions, transactions))
226}
227
228fn js_err<T, E: std::fmt::Display>(result: Result<T, E>) -> Result<T, JsValue> {
229    result.map_err(|e| JsValue::from_str(&e.to_string()))
230}
231
232/// Creates boot transactions.
233/// This function reuses the same boot transaction construction as the psibase CLI, and
234/// is used to generate a wasm that may be called from the browser to construct the boot
235/// transactions when booting the chain from the GUI.
236#[wasm_bindgen]
237pub fn js_create_boot_transactions(
238    producer: String,
239    js_services: JsValue,
240) -> Result<JsValue, JsValue> {
241    let mut services: Vec<PackagedService<Cursor<&[u8]>>> = vec![];
242    let deserialized_services: Vec<ByteBuf> = js_err(serde_wasm_bindgen::from_value(js_services))?;
243    for s in &deserialized_services[..] {
244        services.push(js_err(PackagedService::new(Cursor::new(&s[..])))?);
245    }
246    let now_plus_120secs = chrono::Utc::now() + chrono::Duration::seconds(120);
247    let expiration = TimePointSec::from(now_plus_120secs);
248    let prod = js_err(ExactAccountNumber::from_str(&producer))?;
249
250    // Todo: Allow parameterization of compression level from browser
251    let compression_level = 4;
252    let (boot_transactions, transactions) = js_err(create_boot_transactions(
253        &None,
254        prod.into(),
255        true,
256        expiration,
257        &mut services[..],
258        compression_level,
259    ))?;
260
261    let boot_transactions = boot_transactions.packed();
262    let transactions: Vec<ByteBuf> = transactions
263        .into_iter()
264        .map(|tx| ByteBuf::from(tx.packed()))
265        .collect();
266
267    Ok(serde_wasm_bindgen::to_value(&(
268        ByteBuf::from(boot_transactions),
269        transactions,
270    ))?)
271}