Petal Model
Bloom-native petals are deterministic wasm modules called by the chain executor. They are not ordinary off-chain plugins and they do not have ambient access to files, sockets, environment variables, or subprocesses.
Core Pieces
| Piece | Meaning |
|---|---|
| Petal wasm | The compiled module stored by content hash. |
| Manifest | The bloom_petal_manifest_v0 custom section emitted by #[bloom::petal]. |
| Path | Human-facing binding such as /bloom/core/fungible. |
| Object | Durable chain resource with an ObjectId, TypeTag, owner, version, and payload. |
| Capability | Copyable authority object used to gate privileged functions. |
| PTB | Programmable transaction block: an atomic plan of petal calls and built-in commands. |
Objects
Objects are the durable state model. A petal declares object types with
#[bloom::object], but petal code works with handles such as
Resource<T>, Coin<T>, or Capability<T>.
The chain owns the object table. Petals read and mutate objects through host imports:
object.borrowobject.readobject.mutateobject.createobject.transferobject.delete
Object payloads use the canonical Bloom codec: fixed-width big-endian integers,
length-prefixed bytes and strings, raw 32-byte IDs, and recursive TypeTag
encoding.
Old-style petal payload codecs are not part of the supported model. Petals should not choose their own string widths, hand-pack object fields, or rely on field offsets such as "the value starts at byte 32". The manifest-declared type schema is the decoder.
Type System
Every Bloom value is described by a TypeTag. A TypeTag identifies the type;
the structural definition lives in the petal manifest or in the built-in type
set.
Built-in types use the reserved built-in hash. Their type-argument arities are
fixed, except tuple, whose arity is the number of tuple elements:
| Type | Meaning |
|---|---|
bool, u8, u16, u32, u64, u128 | scalar values |
Address / address, ObjectId, Hash32, UID | raw 32-byte values |
TypeTag | canonical recursive type identity |
String / string | UTF-8 text |
bytes | raw byte sequence |
vector<T> | ordered sequence |
set<T> | canonically ordered unique values |
map<K, V> | canonically ordered key/value entries |
tuple<T0, ...> | positional product value |
Option<T> | None or Some(T) |
Result<T, E> | Ok(T) or Err(E) |
Petal-defined concrete types are declared by #[bloom::object],
#[bloom::capability], or #[derive(BloomType)]. Objects, capabilities, plain
data structs, and enums all record their fields or variants in the manifest.
The codec resolves a concrete TypeTag by first checking built-ins, then
looking up the declaration in the defining petal's manifest.
Generic parameters appear as TypeTag::Generic { idx } inside a declaration and
are substituted with the concrete type arguments of the object or function being
called. TypeTag::External { ref_idx } points through the manifest's
external_type_refs table to a type defined by another petal.
Petal-local self references use petal_hash: [0; 32] only while macro-emitted
code is being built. Built-ins never use that sentinel; they use the reserved
built-in hash. Validator-visible manifests and stored values must resolve self
references to the defining petal's actual content hash.
Canonical Value Encoding
Bloom's value codec is schema-driven and non-self-describing: value bytes do not
carry field names or per-value type tags. The object table, PTB argument slot, or
return slot supplies the TypeTag, and the manifest supplies the structure.
The canonical rules are:
- integers are fixed-width big-endian;
boolis one byte,0or1;Address,ObjectId,Hash32, andUIDare raw 32-byte values;TypeTaguses its own canonical recursive type-tag encoding;- lengths, collection counts, and enum discriminants use minimal ULEB128;
Stringandbytesare ULEB128 byte length followed by bytes;- structs and tuples concatenate fields in declaration order;
vector<T>encodes count, then each element;map<K, V>andset<T>encode count, then entries sorted by each key's canonical encoded bytes, with duplicates rejected;- enums encode the zero-based variant index, then the variant payload;
Option<T>uses0 = None,1 = Some(T);Result<T, E>uses0 = Ok(T),1 = Err(E).
Decoding is strict. Bloom rejects trailing bytes, non-minimal ULEB128, out-of-range enum variants, invalid UTF-8, unsorted or duplicate map/set keys, schema cycles, excessive nesting, and values that exceed call-site limits. A decode failure is an error, not a request to show opaque hex.
Petals should use BloomType::canonical_encode,
BloomType::canonical_decode, and the code generated by #[bloom::object],
#[bloom::capability], and #[derive(BloomType)]. External applications that
submit PTBs, call view functions, or inspect objects should encode and decode
against the petal manifest and this canonical codec. They should not recreate
old ArgReader/RetWriter layouts or assume special cases such as a 48-byte
Coin payload.
Ownership And Linearity
A PTB borrow table tracks every object touched by a transaction. It enforces access modes and prevents double-use of linear values. This is why a PTB can compose several petal calls without copying or losing ownership of resources.
The common access modes are:
- read-only object borrow;
- mutable object borrow;
- consume/move object borrow.
Returned objects are represented as object IDs in command outputs so later PTB
commands can refer to them with Use references.
Capabilities
Capabilities are ordinary objects with special meaning. A function can require a capability argument:
pub fn mint<T>(
_cap: &Capability<MintCap<T>>,
supply: &mut Resource<Supply<T>>,
amount: u128,
) -> Coin<T>The runtime checks that the caller holds a matching capability object. This keeps privileged actions explicit and composable.
Deterministic Chain VM
Chain-mode petals run in a deterministic Wasmtime engine with fuel metering, bounded memory, no threads, no WASI filesystem, and an allow-list of Bloom host imports. Reverts discard the PTB snapshot; successful execution commits the write set.