A Partisia Smart Contract utilizes three distinct binary formats, which are described in detail below.
- RPC Format: When an action of the smart function is invoked, the payload is sent to the action as binary data. The payload identifies which action is invoked and the values for all parameters to the action.
- State Format: The state of a smart contract is stored as binary data in the blockchain state. The state holds the value of all smart contract state variables.
- ABI Format: Meta-information about the smart contract is also stored as binary data, The ABI holds the list of available actions and their parameters and information about the different state variables.
ABI Version changes
- Version 3.1 to 4.1:
- Added
Kind: FnKind
field to FnAbi
.
- Removed
Init
field from ContractAbi
.
- Added zero-knowledge related
FnKind
s, for use in Zk contracts.
- Version 2.0 to 3.1:
- Shortnames are now encoded as LEB128;
ShortnameLength
field have been
removed.
- Added explicit, easily extensible return result format.
The RPC payload identifies which action is invoked and the values for all parameters of the action.
To decode the payload binary format you have to know which argument types each action expects,
since the format of the argument values depends on the argument type.
The ABI Format holds exactly the meta-data needed for finding types of the arguments for each action.
The RPC payload contains the short name identifying the action being called followed by each of the arguments for the action.
\[
\definecolor{mathcolor}{RGB}{33,33,33}
\definecolor{mathgray}{RGB}{100,100,100}
\newcommand{\Rightarrowx}{{\color{mathgray} \ \Rightarrow \ \ }}
\newcommand{\hexi}[1]{{\color{mathcolor}\mathtt{0x}}{\color{mathgray}}{\color{mathcolor}\mathtt{#1}}}
\newcommand{\nnhexi}[1]{{\color{mathcolor}\mathtt{0x}}{\color{mathgray}}{\color{mathcolor} #1}}
\newcommand{\repeat}[2]{#1\text{*}#2}
\newcommand{\byte}{\nnhexi{nn}}
\newcommand{\bytes}[1]{\repeat{\byte{}}{\text{#1}}}
\]
\[
\textcolor{mathcolor}{
\begin{align*}
\text{<PayloadRpc>} \ := \ & \text{action:}\text{ShortName}\ \text{arguments:}\text{ArgumentRpc}\text{*} \Rightarrowx \text{action(arguments)} \\
\end{align*}
}
\]
The short name of an action is an u32 integer identifier that uniquely identifies the action within the smart contract.
The short name is encoded as unsigned LEB128 format, which means that short names have variable lengths.
It is easy to determine how many bytes a LEB128 encoded number contains by examining bit 7 of each byte.
\[
\textcolor{mathcolor}{
\begin{align*}
\text{<ShortName>} \ := \ & \text{pre:}\nnhexi{nn}\text{*}\ \text{last:}\nnhexi{nn}\ \Rightarrowx \text{Action} &\text{(where pre is 0-4 bytes that are ≥0x80 and last<0x80)} \\
\end{align*}
}
\]
The argument binary format depends on the type of the argument. The argument types for each action is defined by the contract, and can be read from the ABI format.
\[
\textcolor{mathcolor}{
\begin{align*}
\text{<ArgumentRpc>} \ :=
\ & \byte{} \ \Rightarrowx \text{u8/i8} & \text{(i8 is twos complement)} \\
| \ & \bytes{2} \ \Rightarrowx \text{u16/i16} & \text{(big endian, i16 is two's complement)} \\
| \ & \bytes{4} \ \Rightarrowx \text{u32/i32} & \text{(big endian, i32 is two's complement)} \\
| \ & \bytes{8} \ \Rightarrowx \text{u64/i64} & \text{(big endian, i64 is two's complement)} \\
| \ & \bytes{16} \ \Rightarrowx \text{u128/i128} & \text{(big endian, i128 is two's complement)} \\
| \ & \text{b:}\byte{} \ \Rightarrowx \text{bool} & \text{(false if b==0, true otherwise)} \\
| \ & \bytes{21} \ \Rightarrowx \text{Address} \\
| \ & \bytes{len} \ \Rightarrowx \text{Array }\text{[u8;len]} & \text{(containing the len u8 values)} \\
| \ & \text{len:}\text{LengthRpc} \ \text{utf8:}\bytes{len} \ \Rightarrowx \text{String} & \text{(with len UTF-8 encoded bytes)} \\
| \ & \text{len:}\text{LengthRpc} \ \text{elems:}\repeat{\text{ArgumentRpc}}{\text{len}} \ \Rightarrowx \text{Vec<>} & \text{(containing the len elements)} \\
| \ & \text{b:}\byte{} \ \text{arg:}\text{ArgumentRpc} \ \Rightarrowx \text{Option<>} & \text{(None if b==0, Some(arg) otherwise)} \\
\end{align*}
}
\]
Only arrays of lengths between (including) 0 and 127 are supported. The high bit in length is reserved for later extensions.
For arguments with variable lengths, such as Vecs or Strings the number of elements is represented as a big endian 32-bit unsigned integer.
\[
\textcolor{mathcolor}{
\begin{align*}
\text{<LengthRpc>} \ := \ & \bytes{4} \ \Rightarrowx \text{u32} &\text{(big endian)} \\
\end{align*}
}
\]
Integers are stored as little-endian. Signed integers are stored as two's
complement. Note that lengths are also stored as little-endian.
\[
\textcolor{mathcolor}{
\begin{align*}
\text{<State>} \ := \ & \nnhexi{nn} \ \Rightarrowx \text{u8/i8} & \text{(i8 is twos complement)} \\
| \ & \bytes{2} \ \Rightarrowx \text{u16/i16} & \text{(little endian, i16 is two's complement)} \\
| \ & \bytes{4} \ \Rightarrowx \text{u32/i32} & \text{(little endian, i32 is two's complement)} \\
| \ & \bytes{8} \ \Rightarrowx \text{u64/i64} & \text{(little endian, i64 is two's complement)} \\
| \ & \bytes{16} \ \Rightarrowx \text{u128/i128} & \text{(little endian, i128 is two's complement)} \\
| \ & \text{b:}\byte{} \ \Rightarrowx \text{bool} & \text{(false if b==0, true otherwise)} \\
| \ & \bytes{21} \ \Rightarrowx \text{Address} \\
| \ & \bytes{len} \ \Rightarrowx \text{Array }\text{[u8;len]} & \text{(containing the len u8 values)} \\
| \ & \text{len:}\text{LengthState} \ \text{utf8:}\bytes{len} \ \Rightarrowx \text{String} & \text{(with len UTF-8 encoded bytes)} \\
| \ & \text{len:}\text{LengthState} \ \text{elems:}\repeat{\text{State}}{\text{len}} \ \Rightarrowx \text{Vec<>} & \text{(containing the len elements)} \\
| \ & \text{b:}\byte{} \ \text{arg:}\text{State} \ \Rightarrowx \text{Option<>} & \text{(None if b==0, Some(arg) otherwise)} \\
| \ & f_1 \text{:State} \dots f_n \text{:State} \Rightarrowx \text{Struct S}\ \{ f_1, f_2, \dots, f_n \} & \\
\end{align*}
}
\]
Only arrays of lengths between (including) 0 and 127 are supported. The high bit in length is reserved for later extensions.
For arguments with variable lengths, such as Vecs or Strings the number of elements is represented as a little endian 32-bit unsigned integer.
\[
\textcolor{mathcolor}{
\begin{align*}
\text{<LengthState>} \ := \ & \bytes{4} \ \Rightarrowx \text{u32} &\text{(little endian)} \\
\end{align*}
}
\]
CopySerializable
A state type is said to be CopySerializable, if it's serialization is
identical to it's in-memory representation, and thus require minimal
serialization overhead. PBC have efficient handling for types that
are CopySerializable. Internal pointers are the main reason that types are not
CopySerializable.
\[
\textcolor{mathcolor}{
\begin{align*}
\text{<CopySerializable>} \ :=
\ & \text{uXXX} \Rightarrowx \text{true}\\
| \ & \text{iXXX} \Rightarrowx \text{true}\\
| \ & \text{bool} \Rightarrowx \text{true}\\
| \ & \text{Address} \Rightarrowx \text{true}\\
| \ & \text{[u8;N]} \Rightarrowx \text{true}\\
| \ & \text{String} \Rightarrowx \text{false}\\
| \ & \text{Vec<T>} \Rightarrowx \text{false}\\
| \ & \text{Option<T>} \Rightarrowx \text{false}\\
| \ & \text{BTreeMap<K, V>} \Rightarrowx \text{false}\\
| \ & \text{BTreeSet<T>} \Rightarrowx \text{false}\\
| \ & \text{Struct S}\ \{ f_1: T_1, \dots, f_n: T_n \} \Rightarrowx \text{CopySerializable}(T_1) \wedge \dots \wedge \text{CopySerializable}(T_n) \wedge \text{WellAligned(S)} \\
\end{align*}
}
\]
The WellAligned constraint on Struct CopySerializable is to guarentee that
struct layouts are identical to serialization. \(\text{Struct S}\ \{ f_1: T_1, \dots, f_n: T_n \}\) is WellAligned if following points hold:
- Annotated with
#[repr(C)]
. Read the Rust Specification
for details on this representation.
- No padding: \(\text{size_of}(S) = \text{size_of}(T_1) + ... + \text{size_of}(T_n)\)
- No wasted bytes when stored in array: \(\text{size_of}(S) \mod \text{align_of}(T_n) = 0\)
It may be desirable to manually add "padding" fields structs in order to
achieve CopySerializable. While this will use extra unneeded bytes for the
serialized state, it may significantly improve serialization speed and lower
gas costs. A future version of the SDK may automatically add padding fields.
Examples
These structs are CopySerializable:
Struct E1 { /* empty */ }
Struct E2 { f1: u32 }
Struct E3 { f1: u32, f2: u32 }
These structs are not CopySerializable:
Struct E4 { f1: u8, f2: u16 }
due to padding between f1 and f2.
Struct E5 { f1: u16, f2: u8 }
due to alignment not dividing size.
Struct E6 { f1: Vec<u8> }
due to non-CopySerializable subfield.
\[
\textcolor{mathcolor}{
\begin{align*}
\text{<TypeSpec>} \ := \ &\text{SimpleTypeSpec} \\
| \ &\text{CompositeTypeSpec} \\
| \ &\text{StructTypeRef} \\
\\
\text{<StructTypeRef>} \ := \ &\hexi{00} \ \text{Index}:\nnhexi{nn} \Rightarrowx StructTypes(\text{Index}) \\
\\
\text{<SimpleTypeSpec>} \ := \ &\hexi{01} \ \Rightarrowx \text{u8} \\
| \ &\hexi{02} \ \Rightarrowx \text{u16} \\
| \ &\hexi{03} \ \Rightarrowx \text{u32} \\
| \ &\hexi{04} \ \Rightarrowx \text{u64} \\
| \ &\hexi{05} \ \Rightarrowx \text{u128} \\
| \ &\hexi{06} \ \Rightarrowx \text{i8} \\
| \ &\hexi{07} \ \Rightarrowx \text{i16} \\
| \ &\hexi{08} \ \Rightarrowx \text{i32} \\
| \ &\hexi{09} \ \Rightarrowx \text{i64} \\
| \ &\hexi{0a} \ \Rightarrowx \text{i128} \\
| \ &\hexi{0b} \ \Rightarrowx \text{String} \\
| \ &\hexi{0c} \ \Rightarrowx \text{bool} \\
| \ &\hexi{0d} \ \Rightarrowx \text{Address} \\
\\
\text{<CompositeTypeSpec>} \ := \ &\hexi{0e} \text{ T:}\text{TypeSpec} \Rightarrowx \text{Vec<}\text{T>} \\
| \ &\hexi{0f} \text{ K:}\text{TypeSpec}\text{ V:}\text{TypeSpec} \Rightarrowx \text{Map <}\text{K}, \text{V>} \\
| \ &\hexi{10} \text{ T:}\text{TypeSpec} \Rightarrowx \text{Set<}\text{T>} \\
| \ &\hexi{11} \text{ L:}\nnhexi{nn} \Rightarrowx \text{[u8; }\text{L}\text{]} & (\hexi{00} \leq L \leq \hexi{7F}) \\
| \ &\hexi{12} \text{ T:}\text{TypeSpec} \Rightarrowx \text{Option<}\text{T>} \\
\\
\end{align*}
}
\]
NOTE: Map
and Set
cannot be used as RPC arguments since it's not possible for a
caller to check equality and sort order of the elements without running the code.
Only arrays of lengths between (including) 0 and 127 are supported. The high bit in length is reserved for later extensions.
All Identifier
names must be valid Rust identifiers; other strings are reserved for future extensions.
\[
\textcolor{mathcolor}{
\begin{align*}
\text{<FileAbi>} \ := \ \{ \
&\text{Header: } \bytes{6}, &\text{The header is always "PBCABI" in ASCII}\\
&\text{VersionBinder: } \bytes{3} \ \\
&\text{VersionClient: } \bytes{3} \ \\
&\text{Contract: ContractAbi} \ \} \\
\\
\text{<ContractAbi>} \ := \ \{ \
&\text{StructTypes: List<StructTypeSpec>}, \\
&\text{Hooks: List<FnAbi>}, \\
&\text{StateType: TypeSpec} \ \} \\
\\
\text{<StructTypeSpec>} \ := \ \{ \
&\text{Name: Identifier}, \\
&\text{Fields: List<FieldAbi>} \ \} \\
\\
\text{<FnAbi>} \ := \ \{ \
&\text{Kind: FnKind}, \\
&\text{Name: Identifier}, \\
&\text{Shortname: LEB128}, \\
&\text{Arguments: List<ArgumentAbi>} \ \} \\
\\
\text{<FieldAbi>} \ := \ \{ \
&\text{Name: Identifier}, \\
&\text{Type: TypeSpec} \ \} \\
\\
\text{<ArgumentAbi>} \ := \ \{ \
&\text{Name: Identifier}, \\
&\text{Type: TypeSpec} \ \} \\
\\
\text{<Identifier>} \ := \ \phantom{\{} \
&\text{len:}\bytes{4} \ \text{utf8:}\bytes{len} & \text{utf8 must be Rust identifier, len is big endian} \\
\\
\text{<LEB128>} \ := \ \phantom{\{} \
&\text{A LEB128 encoded unsigned 32 bit integer (1-5 bytes).} \\
\\
\text{<FnKind>} \ := \ \
&\hexi{01} \ \Rightarrowx \text{Init} &\text{(Num allowed: 1)} \\
|\ &\hexi{02} \ \Rightarrowx \text{Action} &\text{(0..}\infty\text{)}\\
|\ &\hexi{03} \ \Rightarrowx \text{Callback} &\text{(0..}\infty\text{)}\\
|\ &\hexi{10} \ \Rightarrowx \text{ZkSecretInput} &\text{(0..}\infty\text{)}\\
|\ &\hexi{11} \ \Rightarrowx \text{ZkVarInputted} &\text{(0..1)}\\
|\ &\hexi{12} \ \Rightarrowx \text{ZkVarRejected} &\text{(0..1)}\\
|\ &\hexi{13} \ \Rightarrowx \text{ZkComputeComplete} &\text{(0..1)}\\
|\ &\hexi{14} \ \Rightarrowx \text{ZkVarOpened} &\text{(0..1)}\\
|\ &\hexi{15} \ \Rightarrowx \text{ZkUserVarOpened} &\text{(0..1)}\\
|\ &\hexi{16} \ \Rightarrowx \text{ZkAttestationComplete} &text{(0..1)}
\end{align*}
}
\]
Note that a ContractAbi
is only valid if the Hooks
list contains a specific
number of hooks of each type, as specified in FnKind
.
The format used by Wasm contracts to return results is a section-based format defined as following:
\[
\textcolor{mathcolor}{
\begin{align*}
\text{<Result>} \ :=
\ & \text{section}_0\text{: Section} \ \dots \ \text{section}_n\text{: Section} \\
\text{<Section >} \ :=
\ & \text{id:}\byte{} \ \text{len:}\bytes{4} \ \text{data:}\bytes{len} & \ \text{(len is big endian)} \\
\end{align*}
}
\]
Note that section must occur in order of increasing ids. Two ids are
"well-known" and specially handled by the interpreter:
0x01
: Stores event information.
0x02
: Stores state.
Section ids 0x00
to 0x0F
are reserved for "well-known" usage. All others
are passed through the interpreter without modification.