#[service]
Expand description
Define a psibase service interface.
This macro defines the interface to a service so that other services, test cases, and apps which push transactions to the blockchain may use it. It also generates the documentation for the interface, using user-provided documentation as the source.
§Example
/// This service adds and multiplies i32 numbers.
///
/// This is where a detailed description would go.
#[psibase::service]
mod service {
/// Add two numbers together.
///
/// See also [Self::multiply].
#[action]
fn add(a: i32, b: i32) -> i32 {
a + b
}
/// Multiplies two numbers together.
///
/// See also [Self::add].
#[action]
fn multiply(a: i32, b: i32) -> i32 {
a * b
}
}
The service module and the actions within it may be private; the macro creates public definitions (below).
The macro copies the action documentation (like above) to the
Actions<T>
methods. Use the [Self::...]
syntax like above within action documentation to refer to
other actions.
§Recursion Safety
The recursive
option, which defaults to false,
controls whether the service can be reentered while it’s
currently executing. This prevents a series of exploits
based on this pattern:
- Service
A
calls ServiceB
- Service
B
calls back into ServiceA
Service A
may opt into allowing recursion by setting the
recursive
option to true. This requires very careful
design to prevent exploits. The following is a non-exhaustive
list of potential attacks:
A
writes to a table, callsB
, then writes to another table. Since it was in the middle of writing,A
’s overall state is inconsistent.B
calls a method onA
which malfunctions because of the inconsistency between the two tables.A
reads some rows from a table then callsB
.B
calls an action inA
which modifies the table. WhenB
returns,A
relies on the previously-read, but now out of date, data.A
callsB
while iterating through a table index.B
calls an action inA
which modifies the table. WhenB
returns, the iteration is now in an inconsistent state.
Rust’s borrow checker doesn’t prevent these attacks since
nothing is mutably borrowed long term. Tables wrap psibase’s
kv native functions,
which treat the underlying KV store as if it were in an
UnsafeCell
. The Rust table wrappers can’t protect against
this since it’s possible, and normal under recursion, to create
multiple wrappers covering the same data range.
§Generated Output
The macro adds the following definitions to the service module:
pub const SERVICE: psibase::AccountNumber;
pub struct Wrapper;
pub struct Actions<T: psibase::Caller>;
pub mod action_structs;
mod service_wasm_interface;
It reexports SERVICE
, Wrapper
, Actions
, and action_structs
as public in
the parent module. These names are configurable.
§SERVICE constant
The SERVICE
constant identifies the account the service is normally installed on. The
macro generates this from the package name, but this can be overridden using the name
option.
§Wrapper struct
pub struct Wrapper;
The Wrapper
struct makes it easy for other services, test cases, and Rust applications
to call into the service. It has the following implementation:
impl Wrapper {
// The account this service normally runs on
pub const SERVICE: psibase::AccountNumber;
// Call another service.
//
// `call_*` methods return an object which has methods (one per action) which
// call another service and return the result from the call. These methods are
// only usable by services.
pub fn call() -> Actions<psibase::ServiceCaller>;
pub fn call_to(service: psibase::AccountNumber)
-> Actions<psibase::ServiceCaller>;
pub fn call_from(sender: psibase::AccountNumber)
-> Actions<psibase::ServiceCaller>;
pub fn call_from_to(
sender: psibase::AccountNumber,
service: psibase::AccountNumber)
-> Actions<psibase::ServiceCaller>;
// push transactions to psibase::Chain.
//
// `push_*` methods return an object which has methods (one per action) which
// push transactions to a test chain and return a psibase::ChainResult or
// psibase::ChainEmptyResult. This final object can verify success or failure
// and can retrieve the return value, if any.
pub fn push(
chain: &psibase::Chain,
) -> Actions<psibase::ChainPusher>;
pub fn push_to(
chain: &psibase::Chain,
service: psibase::AccountNumber,
) -> Actions<psibase::ChainPusher>;
pub fn push_from(
chain: &psibase::Chain,
sender: psibase::AccountNumber,
) -> Actions<psibase::ChainPusher>;
pub fn push_from_to(
chain: &psibase::Chain,
sender: psibase::AccountNumber,
service: psibase::AccountNumber,
) -> Actions<psibase::ChainPusher>;
// Pack actions into psibase::Action.
//
// `pack_*` functions return an object which has methods (one per action)
// which pack the action's arguments using fracpack and return a psibase::Action.
// The `pack_*` series of functions is mainly useful to applications which
// push transactions to blockchains.
pub fn pack() -> Actions<psibase::ActionPacker>;
pub fn pack_to(
service: psibase::AccountNumber,
) -> Actions<psibase::ActionPacker>;
pub fn pack_from(
sender: psibase::AccountNumber,
) -> Actions<psibase::ActionPacker>;
pub fn pack_from_to(
sender: psibase::AccountNumber,
service: psibase::AccountNumber,
) -> Actions<psibase::ActionPacker>;
}
§Actions struct
pub struct Actions<T: psibase::Caller> {
pub caller: T,
}
This struct’s implementation contains a public method for each action. The methods have
the same names and arguments as the actions. The methods pass their arguments as a tuple
to either Caller::call
or Caller::call_returns_nothing
, returning the final result.
Actions<T>
is part of the glue which makes Wrapper
work; Wrapper
methods return
Actions<T>
instances with the appropriate inner caller
. Actions<T>
also documents
the actions themselves.
§action_structs module
pub mod action_structs {...}
action_structs
contains a public struct for each action. Each struct has the same
name as its action and has the same fields as the action’s arguments. The structs
implement fracpack::Packable
.
§service_wasm_interface module
This module defines the start
and called
WASM entry points. psinode
calls start
to initialize the WASM whenever it is used within a
transaction. psinode calls called
every time another service calls into
this WASM. called
deserializes action data, calls into the appropriate
action function, and serializes the return value.
§Dead code warnings
When the dispatch option is false, there is usually no code
remaining which calls the actions. The service macro adds #[allow(dead_code)]
to the service module when the dispatch option is false to prevent the
compiler from warning about it.
§Options
The service attribute has the following options. The defaults are shown:
#[psibase::service(
name = see_blow, // Account service is normally installed on
recursive = false, // Allow service to be recursively entered?
constant = "SERVICE", // Name of generated constant
actions = "Actions", // Name of generated struct
wrapper = "Wrapper", // Name of generated struct
structs = "action_structs", // Name of generated module
dispatch = see_below, // Create service_wasm_interface?
pub_constant = true, // Make constant public and reexport it?
)]
name
defaults to the package name.
dispatch
defaults to true if the CARGO_PRIMARY_PACKAGE
environment
variable is set, and false otherwise. Cargo sets this variable automatically.
For example, assume you have two services, A and B. B brings in A as a
dependency so it can use A’s wrappers to call it. When cargo builds A,
dispatch
will default to true in A’s service definition. When cargo builds
B, dispatch
will default to true in B’s service definition but false
in A’s. This prevents B from accidentally including A’s dispatch.
If the CARGO_PSIBASE_TEST
environment variable is set, then the macro
forces dispatch
to false. cargo psibase test
sets CARGO_PSIBASE_TEST
to prevent tests from having service entry points.