On January 30th the ancient Stellar blockchain is getting "Soroban" smart contract support and thus entering a brave new world of arbitrary code execution. As someone who's been involved with Stellar since 2014 I could not be more excited. 10 years. TEN YEARS!! and we're finally being gifted the entrepreneurial utopia of decentralized compute on the blockchain for real world.
This January 30th day kicks off a Phase 0 of several ramp up phases as Soroban receives more and more of the block interest as the network ensures a safe and stable rollout of the madness that is Soroban. These phases each introduce their own set of resource limitations on both the network from and on the protocol itself. This is the topic of interest for this post. Phase 0 is really resource constrained. (See InitialSorobanNetworkConfig).
I've been waiting for this day for 10 years though so you'd better believe the only question on my mind was "okay yeah sure, it's limited, but is it possible to actually upload and invoke a contract in Phase 0?"
Answer: Yes
Step 1: Stand up a Phase 0 testing environment
The first challenge I was faced with was how to simulate a Phase 0 network limitations. Both Futurenet and Testnet are currently running Phase 1 so it's actually non-trivial to test for all the limits on a network that allows for significantly more than Phase 0.
Enter the Docker quickstart! Turns out some genius SDF engineer added a --limits flag with a default setting which was exactly what I was looking for.
docker run --rm -i \
-p "8000:8000" \
--name stellar \
stellar/quickstart:testing \
--local \
--limits default \
--enable-soroban-rpcAnd we're off to the races with a local Phase 0 network!
Step 2: Develop a really small contract
Next on the list is sorting out creating a really small contract. Small in this instance means small in every way. Small final contract size. Small invocation response size. Small number of resources consumed.
This particular step led me deep down a rabbit hole of writing WASM by hand. See it turns out that while Soroban is wrapped all up in Rust it's actually running WebAssembly behind the scenes. What this means is you can ditch the Rust in the end and just write WASM code. This would be madness for a contract of any substance, but for our purposes trying to get to the absolute smallest invocable contract this should be fine.
Let's start with the smallest Rust contract code I can think of:
#![no_std]
use soroban_sdk::{contract, contractimpl, Symbol, symbol_short};
#[contract]
pub struct Contract;
#[contractimpl]
impl Contract {
pub fn call() -> Symbol {
symbol_short!("hi")
}
}Once we build and optimize that contract:
soroban contract build
soroban contract optimize --wasm phase0.wasmWe get back a 300 byte contract. That's really small and actually probably entirely tiny enough. However, I'm obsessive, and want to be able to claim something as bold and outrageous and having deployed and invoked the world's smallest smart contract and at this point I'm not convinced 300 is the absolute floor. So I dig in deeper.
What I really need at this point is the ability to observe and dissect my raw WASM file and see what it's made of. Enter wabt and it's suite of WASM dissection and manipulation tools! With that installed I can begin to inspect my 300 byte contract to see what it contains.
wasm-objdump phase0.wasm -xsProbably way more than we need but whatever. I found the Contents of section {x}: data particularly interesting. Especially the Custom sections. There's a lot of bytes in the contractmetav0 section. There's also some functions and exports I thought may be unnecessary for such an otherwise simple and relatively useless contract. But we're after every possible size reduction possible so let's start shaving off some of this potential cruft and rebuilding the WASM contract back up section by section.
Before we do that let's take a look at the actual code portion of the WASM by using the wasm2wat tool from the wabt kit
wasm2wat phase0.wasm -o phase0.watOkay interesting yeah let's start by blasting off things I don't actually think we need:
Which will leave us with just:
(module
(type (;0;) (func (result i64)))
(func (;0;) (type 0) (result i64)
i64.const 749070)
(export "call" (func 0)))From there let's reassemble this slimmed down WASM:
wat2wasm phase0.wat -o phase0-slim.wasmLet's take another look at the wasm:
wasm-objdump phase0-slim.wasm -xsAlright tight that's looking way smaller! It's actually just 39 bytes! Those custom sections were adding a lot!
Onward to attempt to deploy and invoke this as the world's smallest smart contract!
Step 3: Install, create and invoke the smart contract
In order to get to an invocable contract we actually need to do two things first. The first is to install the WASM code and the second is to create a new contract instance which references that code as it's callable code. Contracts and their code live as two separate entries on the blockchain.
Install
I may enjoy Rust but JS is my true love so I'll be using the Stellar JS SDK to accomplish this step. We won't get too much into the nitty gritty of the specific SDK syntax for submitting transactions. I'll just give you the code and then focus on the more interesting bits for assembling the WASM and installing it onto the Stellar blockchain. I use TypeScript but I also hate build tools so I'll be using Bun in order to keep this whole thing in a single file. After installing Bun I got my project started with bun init. From there my index.ts file looks like this.
asnyc function sendOp
All you really need to care about is that inside the //// Start WASM Assembly we are getting the binary of our phase0-slim.wasm contract and then inside the //// Start WASM Install we create a Stellar transaction with an uploadContractWasm operation which will install our WASM contract code onto the blockchain. Let's run it!
bun run index.tsSweet licorice fish sticks! Okay looking looking aha.
...
0: [Diagnostic Event] topics:[error, Error(WasmVm, InvalidInput)], data:\"contract missing metadata section\"
...Looks like maybe one or more of those custom sections aren't as optional as I'd hoped. I'll save you the leg work and just tell you contractspecv0 and contractmetav0 are optional however contractenvmeta is required. It's worth noting that none of these custom sections are arbitrarily added though. contractspecv0 is essential if you want to have any sort of public facing interface for your contract. Without it any interface would be flying blind on how to interact with the contract. So it's important, but not required. The same goes for that fat contractmetav0 which contains a bunch of versioning information which will be really important for backwards compatibility as Soroban adds new versions in the future. But again we're going for record smashing small so toss it all and leave only the essentials!
The nice thing is that the contractenvmeta isn't dynamic per contract. As long as your major Soroban environment version stays the same this will remain static. As such here's that custom section in it's array format which we'll then use to concatenate onto our actual contract code.
[0,30,17,99,111,110,116,114,97,99,116,101,110,118,109,101,116,97,118,48,0,0,0,0,0,0,0,20,0,0,0,0]Cool. Let's modify our //// Start WASM Assembly now to append this custom slice onto the end of the contract binary.
//// Start WASM Assembly
+ const meta = [0,30,17,99,111,110,116,114,97,99,116,101,110,118,109,101,116,97,118,48,0,0,0,0,0,0,0,20,0,0,0,0]
const path = './phase0.wasm'
const file = Bun.file(path)
- const wasm = Buffer.from(await file.arrayBuffer())
+ const wasm = Buffer.concat([
+ Buffer.from(await file.arrayBuffer()),
+ Buffer.from(meta)
+ ])
console.log(wasm.length)
//// End WASM AssemblyNice! Now let's try bun run index.ts again:
71
b713963af003af00228f91eee44bf031d29eb545e7361a59f7c1d731a65d12f4Okay! We've install a binary of 71 bytes to the Stellar blockchain! That's insane! I wonder though... Could we make it even !?
Side Quest
Create
With the contract code successfully installed we can now move to creating a contract instance which points to this code as it's executable. This contract is then what we'll actually invoke in the end to get our hi value back.
With these updates saved let's call our trusty bun run index.ts again.
71
b713963af003af00228f91eee44bf031d29eb545e7361a59f7c1d731a65d12f4
CBZ2CFNNI7TOOO5LHIN7YJDG4BEX7UO75XCR2NZRZQR2SCUFD2TRJFEVAwesome! With that there remains only one final step standing between us and sweet sweet victory!
Invoke
Lets add the contract invocation logic.
And now 🤞 let's try one final bun run index.ts
71
b713963af003af00228f91eee44bf031d29eb545e7361a59f7c1d731a65d12f4
CBUR5YZ6AKDMVIYEBCMID5YGUBPBK5NGQMCMJMK3V5MZHH6CR6NHYCL7
hiLet's goooo!!!
A valid, invocable, 71 byte smart contract. Absolutely insane!
Just one more thing
Big thanks to @heytdep and this friend for helping me understand WASM in deeper ways than I ever thought I wanted to ♥️