pink_extension/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#![cfg_attr(not(feature = "std"), feature(alloc_error_handler))]
3#![doc = include_str!("../README.md")]
4
5#[macro_use]
6extern crate alloc;
7
8use alloc::string::String;
9use alloc::vec::Vec;
10
11use ink::env::{emit_event, topics::state::HasRemainingTopics, Environment, Topics};
12
13use ink::EnvAccess;
14use scale::{Decode, Encode};
15
16pub use pink_extension_macro::{contract, driver};
17
18pub mod chain_extension;
19pub use chain_extension::pink_extension_instance as ext;
20pub mod logger;
21pub mod system;
22
23#[cfg(all(not(feature = "std"), feature = "dlmalloc"))]
24mod allocator_dlmalloc;
25
26pub use logger::ResultExt;
27use serde::{Deserialize, Serialize};
28
29const PINK_EVENT_TOPIC: &[u8] = b"phala.pink.event";
30
31pub type WorkerId = [u8; 32];
32pub type EcdhPublicKey = [u8; 32];
33pub type Hash = [u8; 32];
34pub type EcdsaPublicKey = [u8; 33];
35pub type EcdsaSignature = [u8; 65];
36pub type AccountId = <PinkEnvironment as Environment>::AccountId;
37pub type Balance = <PinkEnvironment as Environment>::Balance;
38pub type BlockNumber = <PinkEnvironment as Environment>::BlockNumber;
39
40pub trait ConvertTo<To> {
41    fn convert_to(&self) -> To;
42}
43
44impl<F, T> ConvertTo<T> for F
45where
46    F: AsRef<[u8; 32]>,
47    T: From<[u8; 32]>,
48{
49    fn convert_to(&self) -> T {
50        (*self.as_ref()).into()
51    }
52}
53
54/// A phala-mq message
55#[derive(Encode, Decode, Debug)]
56#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
57pub struct Message {
58    pub payload: Vec<u8>,
59    pub topic: Vec<u8>,
60}
61
62/// A phala-mq message with optional encryption key
63#[derive(Encode, Decode, Debug)]
64#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
65pub struct OspMessage {
66    pub message: Message,
67    pub remote_pubkey: Option<EcdhPublicKey>,
68}
69
70/// Hook points defined in the runtime.
71#[derive(Encode, Decode, Debug, Clone)]
72#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
73pub enum HookPoint {
74    /// When all events in a block are processed.
75    OnBlockEnd,
76}
77
78/// System Event used to communicate between the contract and the runtime.
79#[derive(Encode, Decode, Debug, Clone)]
80#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
81pub enum PinkEvent {
82    /// Set contract hook
83    ///
84    /// Please do not use this event directly, use [`set_hook()`] instead.
85    ///
86    /// # Availability
87    /// System contract
88    #[codec(index = 2)]
89    SetHook {
90        /// The event to hook
91        hook: HookPoint,
92        /// The target contract address
93        contract: AccountId,
94        /// The selector to invoke on hooked event fired.
95        selector: u32,
96        /// The gas limit when calling the selector
97        gas_limit: u64,
98    },
99    /// Deploy a sidevm instance to given contract instance
100    ///
101    /// Please do not use this event directly, use [`deploy_sidevm_to()`] instead.
102    ///
103    /// # Availability
104    /// System contract
105    #[codec(index = 3)]
106    DeploySidevmTo {
107        /// The target contract address
108        contract: AccountId,
109        /// The hash of the sidevm code.
110        code_hash: Hash,
111    },
112    /// Push a message to the associated sidevm instance.
113    ///
114    /// Please do not use this event directly, use [`push_sidevm_message()`] instead.
115    ///
116    /// # Availability
117    /// Any contract
118    #[codec(index = 4)]
119    SidevmMessage(Vec<u8>),
120    /// Instructions to manipulate the cache. Including set, remove and set expiration.
121    ///
122    /// # Availability
123    /// Any contract
124    #[codec(index = 5)]
125    CacheOp(CacheOp),
126    /// Stop the side VM instance associated with the caller contract if it is running.
127    ///
128    /// Please do not use this event directly, use [`force_stop_sidevm()`] instead.
129    ///
130    /// # Availability
131    /// Any contract
132    #[codec(index = 6)]
133    StopSidevm,
134    /// Force stop the side VM instance associated with the given contract if it is running.
135    ///
136    /// Please do not use this event directly, use [`stop_sidevm_at()`] instead.
137    ///
138    /// # Availability
139    /// System contract
140    #[codec(index = 7)]
141    ForceStopSidevm {
142        /// The target contract address
143        contract: AccountId,
144    },
145    /// Set the log handler contract for current cluster.
146    ///
147    /// Please do not use this event directly, use [`set_log_handler()`] instead.
148    ///
149    /// # Availability
150    /// System contract
151    #[codec(index = 8)]
152    SetLogHandler(AccountId),
153    /// Set the weight of contract used to schedule queries and sidevm virtual runtime
154    ///
155    /// Please do not use this event directly, use [`set_contract_weight()`] instead.
156    ///
157    /// # Availability
158    /// System contract
159    #[codec(index = 9)]
160    SetContractWeight { contract: AccountId, weight: u32 },
161    /// Upgrade the runtime to given version
162    ///
163    /// Please do not use this event directly, use [`upgrade_runtime()`] instead.
164    ///
165    /// # Availability
166    /// System contract
167    #[codec(index = 10)]
168    UpgradeRuntimeTo { version: (u32, u32) },
169    /// Deploy a sidevm instance to given contract instance on given workers.
170    ///
171    /// # Availability
172    /// System contract
173    #[codec(index = 11)]
174    SidevmOperation(SidevmOperation),
175    /// Set the Sidevm program that used to evaluate js code.
176    ///
177    /// Please do not use this event directly, use [`set_js_runtime()`] instead.
178    ///
179    /// # Availability
180    /// System contract
181    #[codec(index = 12)]
182    SetJsRuntime(Hash),
183}
184
185#[derive(Encode, Decode, Debug, Clone)]
186#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
187pub enum SidevmOperation {
188    Start {
189        /// The target contract address
190        contract: AccountId,
191        /// The hash of the sidevm code.
192        code_hash: Hash,
193        /// The workers to deploy the sidevm instance.
194        workers: Workers,
195        config: SidevmConfig,
196    },
197    SetDeadline {
198        /// The target contract address
199        contract: AccountId,
200        /// The block number that the SideVM instance is allowed to run until.
201        deadline: u32,
202    },
203}
204
205#[derive(Encode, Decode, Serialize, Deserialize, Debug, Clone)]
206#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
207pub struct SidevmConfig {
208    /// The maximum size of the code that can be uploaded to the SideVM instance.
209    pub max_code_size: u32,
210    /// The maximum number of memory pages(64KB per page) that can be allocated by the SideVM instance.
211    pub max_memory_pages: u32,
212    /// The max gas between two host function calls for the SideVM instance.
213    pub vital_capacity: u64,
214    /// The block number that the SideVM instance is allowed to run until.
215    pub deadline: u32,
216}
217
218impl Default for SidevmConfig {
219    fn default() -> Self {
220        Self {
221            max_code_size: 1024 * 1024 * 10,
222            // 64MB
223            max_memory_pages: 1024,
224            // about 20 ms
225            vital_capacity: 50_000_000_000_u64,
226            deadline: u32::MAX,
227        }
228    }
229}
230
231#[derive(Encode, Decode, Debug, Clone)]
232#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
233pub enum Workers {
234    All,
235    List(Vec<WorkerId>),
236}
237
238impl PinkEvent {
239    pub fn allowed_in_query(&self) -> bool {
240        match self {
241            PinkEvent::SetHook { .. } => false,
242            PinkEvent::DeploySidevmTo { .. } => true,
243            PinkEvent::SidevmMessage(_) => true,
244            PinkEvent::CacheOp(_) => true,
245            PinkEvent::StopSidevm => true,
246            PinkEvent::ForceStopSidevm { .. } => true,
247            PinkEvent::SetLogHandler(_) => false,
248            PinkEvent::SetContractWeight { .. } => false,
249            PinkEvent::UpgradeRuntimeTo { .. } => false,
250            PinkEvent::SidevmOperation(_) => true,
251            PinkEvent::SetJsRuntime(_) => false,
252        }
253    }
254
255    pub fn name(&self) -> &'static str {
256        match self {
257            PinkEvent::SetHook { .. } => "SetHook",
258            PinkEvent::DeploySidevmTo { .. } => "DeploySidevmTo",
259            PinkEvent::SidevmMessage(_) => "SidevmMessage",
260            PinkEvent::CacheOp(_) => "CacheOp",
261            PinkEvent::StopSidevm => "StopSidevm",
262            PinkEvent::ForceStopSidevm { .. } => "ForceStopSidevm",
263            PinkEvent::SetLogHandler(_) => "SetLogHandler",
264            PinkEvent::SetContractWeight { .. } => "SetContractWeight",
265            PinkEvent::UpgradeRuntimeTo { .. } => "UpgradeRuntimeTo",
266            PinkEvent::SidevmOperation(_) => "SidevmOperation",
267            PinkEvent::SetJsRuntime(_) => "SetJsRuntime",
268        }
269    }
270
271    pub fn is_private(&self) -> bool {
272        match self {
273            PinkEvent::SetHook { .. } => false,
274            PinkEvent::DeploySidevmTo { .. } => false,
275            PinkEvent::SidevmMessage(_) => true,
276            PinkEvent::CacheOp(_) => true,
277            PinkEvent::StopSidevm => false,
278            PinkEvent::ForceStopSidevm { .. } => false,
279            PinkEvent::SetLogHandler(_) => false,
280            PinkEvent::SetContractWeight { .. } => false,
281            PinkEvent::UpgradeRuntimeTo { .. } => false,
282            PinkEvent::SidevmOperation(_) => false,
283            PinkEvent::SetJsRuntime(_) => false,
284        }
285    }
286}
287
288/// Instructions to manipulate the cache.
289#[derive(Encode, Decode, Debug, Clone)]
290#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
291pub enum CacheOp {
292    /// Set a key-value pair in the cache.
293    Set { key: Vec<u8>, value: Vec<u8> },
294    /// Set the expiration of a key-value pair in the cache.
295    SetExpiration { key: Vec<u8>, expiration: u64 },
296    /// Remove a key-value pair from the cache.
297    Remove { key: Vec<u8> },
298}
299
300impl Topics for PinkEvent {
301    type RemainingTopics = [HasRemainingTopics; 1];
302
303    fn topics<E, B>(
304        &self,
305        builder: ink::env::topics::TopicsBuilder<ink::env::topics::state::Uninit, E, B>,
306    ) -> <B as ink::env::topics::TopicsBuilderBackend<E>>::Output
307    where
308        E: Environment,
309        B: ink::env::topics::TopicsBuilderBackend<E>,
310    {
311        builder
312            .build::<Self>()
313            .push_topic(&PINK_EVENT_TOPIC)
314            .finish()
315    }
316}
317
318#[cfg(feature = "runtime_utils")]
319impl PinkEvent {
320    pub fn event_topic() -> Hash {
321        use std::convert::TryFrom;
322        let topics = topic::topics_for(Self::StopSidevm);
323        let topic: &[u8] = topics[0].as_ref();
324        Hash::try_from(topic).expect("Should not failed")
325    }
326}
327
328/// Sets a hook receiver for a given hook point.
329///
330/// A hook is a mechanism that allows certain actions to be triggered when a specific situation arises.
331/// When the situation corresponding to the hook point occurs, the runtime will call the receiver contract
332/// using the specified selector.
333///
334/// # Supported Hook Points
335///  - `OnBlockEnd`: The receiver contract will be invoked once all events in a Phala chain block have been processed.
336///
337/// # Arguments
338///
339/// * `hook`: The hook point for which the receiver is set.
340/// * `contract`: The AccountId of the contract to be called when the hook is triggered.
341/// * `selector`: The function selector to be used when calling the receiver contract.
342/// * `gas_limit`: The maximum amount of gas that can be used when calling the receiver contract.
343///
344/// Note: The cost of the execution would be charged to the contract itself.
345///
346/// This api is only available for the system contract. User contracts should use `System::set_hook` instead.
347pub fn set_hook(hook: HookPoint, contract: AccountId, selector: u32, gas_limit: u64) {
348    emit_event::<PinkEnvironment, _>(PinkEvent::SetHook {
349        hook,
350        contract,
351        selector,
352        gas_limit,
353    })
354}
355
356/// Starts a SideVM instance with the provided code hash.
357///
358/// The calling contract must be authorized by the `SidevmOperation` driver contract.
359///
360/// If the code corresponding to the provided hash hasn't been uploaded to the cluster storage yet,
361/// it will create an empty SideVM instance. This instance will wait for the code to be uploaded
362/// via `prpc::UploadSidevmCode`.
363///
364///# Arguments
365///
366///* `code_hash`: The hash of the code to be used for starting the SideVM instance.
367///
368///# Returns
369///
370/// A `Result` indicating success or failure, specifically a `system::DriverError` in case of failure.
371pub fn start_sidevm(code_hash: Hash) -> Result<(), system::DriverError> {
372    let driver =
373        crate::system::SidevmOperationRef::instance().ok_or(system::Error::DriverNotFound)?;
374    driver.deploy(code_hash)
375}
376
377/// Deploy a SideVM instance to a given contract. (system only)
378pub fn deploy_sidevm_to(contract: AccountId, code_hash: Hash) {
379    emit_event::<PinkEnvironment, _>(PinkEvent::DeploySidevmTo {
380        contract,
381        code_hash,
382    });
383}
384
385/// Stop a SideVM instance running at given contract address. (system only)
386pub fn stop_sidevm_at(contract: AccountId) {
387    emit_event::<PinkEnvironment, _>(PinkEvent::ForceStopSidevm { contract });
388}
389
390/// Force stop the side VM instance if it is running
391///
392/// You should avoid to call this function. Instead, prefer let the side program exit gracefully
393/// by itself.
394pub fn force_stop_sidevm() {
395    emit_event::<PinkEnvironment, _>(PinkEvent::StopSidevm)
396}
397
398/// Pushes a message to the associated SideVM instance.
399///
400/// Note: There is no guarantee that the message will be received by the SideVM instance.
401/// The message may be dropped due to several reasons:
402///
403/// - The SideVM instance is not currently running.
404/// - The SideVM instance is running, but the message queue is full. This may occur when the SideVM
405///   instance is busy processing other messages.
406///
407///# Arguments
408///
409///* `message`: The message to be pushed to the SideVM instance.
410pub fn push_sidevm_message(message: Vec<u8>) {
411    emit_event::<PinkEnvironment, _>(PinkEvent::SidevmMessage(message))
412}
413
414/// Set the log handler contract of current cluster. (system only)
415pub fn set_log_handler(contract: AccountId) {
416    emit_event::<PinkEnvironment, _>(PinkEvent::SetLogHandler(contract))
417}
418
419/// Set the SideVM program that used by js_eval code. (system only)
420pub fn set_js_runtime(code_hash: Hash) {
421    emit_event::<PinkEnvironment, _>(PinkEvent::SetJsRuntime(code_hash))
422}
423
424/// Set the weight of contract used to schedule queries and sidevm virtual runtime. (system only)
425pub fn set_contract_weight(contract: AccountId, weight: u32) {
426    emit_event::<PinkEnvironment, _>(PinkEvent::SetContractWeight { contract, weight });
427}
428
429/// Upgrade the pink runtime to given version. (system only)
430///
431/// Note: pRuntime would exit if the version is not supported.
432pub fn upgrade_runtime(version: (u32, u32)) {
433    emit_event::<PinkEnvironment, _>(PinkEvent::UpgradeRuntimeTo { version });
434}
435
436/// Generate a slice of verifiable random bytes.
437///
438/// When called in a contract with the same salt, the same random bytes will be generated.
439/// Different contracts with the same salt will generate different random bytes.
440///
441/// # Availability
442/// any contract | query | transaction
443pub fn vrf(salt: &[u8]) -> Vec<u8> {
444    let mut key_salt = b"vrf:".to_vec();
445    key_salt.extend_from_slice(salt);
446    ext().derive_sr25519_key(key_salt.into())
447}
448
449/// Query to a sidevm in current worker.
450pub fn query_local_sidevm(address: AccountId, payload: Vec<u8>) -> Result<Vec<u8>, String> {
451    let url = format!("sidevm://{}", hex::encode(address));
452    let response = http_post!(url, payload);
453    if response.status_code != 200 {
454        return Err(format!(
455            "SideVM query failed: {} {}: {}",
456            response.status_code,
457            response.reason_phrase,
458            String::from_utf8_lossy(&response.body)
459        ));
460    }
461    Ok(response.body)
462}
463
464/// Pink defined environment. This environment is used to access the phat contract extended runtime features.
465///
466/// # Example
467/// ```
468/// #[ink::contract(env = PinkEnvironment)]
469/// mod my_contract {
470///     use pink_extension::PinkEnvironment;
471///     #[ink(storage)]
472///     pub struct MyContract {}
473///     impl MyContract {
474///         #[ink(constructor)]
475///         pub fn new() -> Self {
476///             Self {}
477///         }
478///         #[ink(message)]
479///         pub fn my_message(&self) {
480///             // Access the pink environment.
481///             let _pink_version = self.env().extension().runtime_version();
482///         }
483///     }
484/// }
485/// ```
486#[derive(Debug, Clone, PartialEq, Eq)]
487#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
488pub enum PinkEnvironment {}
489
490impl Environment for PinkEnvironment {
491    const MAX_EVENT_TOPICS: usize = <ink::env::DefaultEnvironment as Environment>::MAX_EVENT_TOPICS;
492
493    type AccountId = <ink::env::DefaultEnvironment as Environment>::AccountId;
494    type Balance = <ink::env::DefaultEnvironment as Environment>::Balance;
495    type Hash = <ink::env::DefaultEnvironment as Environment>::Hash;
496    type BlockNumber = <ink::env::DefaultEnvironment as Environment>::BlockNumber;
497    type Timestamp = <ink::env::DefaultEnvironment as Environment>::Timestamp;
498
499    type ChainExtension = chain_extension::PinkExt;
500}
501
502/// Returns the PinkEnvironment.
503pub fn env() -> EnvAccess<'static, PinkEnvironment> {
504    Default::default()
505}
506
507#[cfg(feature = "runtime_utils")]
508mod topic;
509
510#[cfg(test)]
511mod tests {
512    #[test]
513    fn test_event_topics() {
514        insta::assert_debug_snapshot!(super::PinkEvent::event_topic());
515    }
516}