To provide a hands on gamified approach to tinker with soroban, the latest series of Stellar Quest (fca00c: Asteroids) was all about submitting a compiled WASM to compete in three categories:
- earliest working submission
- smallest working submission
- most efficient (CPU cycles) working submission
Optimizing the contract
Although soroban already offers pretty resource-oriented logging capabilites (e.g. strips everything related to logging from release builds), all the logic around making logs readable would still stay in the contract and bloat the file size.
So our approach here is to keep everything log-related out of the solution (that was supposed to be submitted) and use a proxy between the solution and the game-engine provided. To achieve that we can make use of a proxy that logs all the commands before invoking them on the actual engine.
A few things need to be considered here:
- release builds (as described/configured per soroban docs) strip the
log!
macro related code - we want the proxy contract to always log - even if used by a release solution
- as the proxy is another soroban contract, the same limitations regarding logging/formatting apply due to
std
not being available
For sake of context let's just add the contract next to the solution contract but keep it out of the workspace:
β― tree contracts/ -L 2
contracts/
βββ _game_engine
β βββ engine.rs
β βββ lib.rs
β βββ map.rs
β βββ storage.rs
β βββ types.rs
βββ game_engine.wasm
βββ logging_engine
β βββ Cargo.lock
β βββ Cargo.toml
β βββ src
βββ solution
βββ Cargo.toml
βββ src
5 directories, 9 files
β―
That way we can individually configure the contract to always build with debug-assertions = true
but we don't need to deal with configuring this as a workspace package with profile-overrides.
Implementing the engine contract
First of all the original game_engine
contract-interface needs to be implemented so that our logging_engine
provides all the expected functions defined by the game_engine::Contract
.
By using impl Contract for LoggingEngine
the complete game_engine
interface is enforced to be implemented.
contracts/logging_engine/src/engine.rs:
show file listing
use soroban_sdk::{contractimpl, Env, Map};
mod game_engine {
soroban_sdk::contractimport!(file = "../game_engine.wasm");
}
use game_engine::Contract;
pub struct LoggingEngine;
#[contractimpl]
impl Contract for LoggingEngine {
fn init(
env: Env,
move_step: u32,
laser_range: u32,
seed: u64,
view_range: u32,
fuel: (u32, u32, u32, u32),
asteroid_reward: u32,
asteroid_density: u32,
pod_density: u32,
) {
todo!("needs implementation")
}
fn p_turn(env: Env, direction: game_engine::Direction) -> Result<(), game_engine::Error> {
todo!("needs implementation")
}
fn p_move(env: Env, times: Option<u32>) -> Result<(), game_engine::Error> {
todo!("needs implementation")
}
fn p_shoot(env: Env) -> Result<(), game_engine::Error> {
todo!("needs implementation")
}
fn p_harvest(env: Env) -> Result<(), game_engine::Error> {
todo!("needs implementation")
}
fn p_upgrade(env: Env) -> Result<(), game_engine::Error> {
todo!("needs implementation")
}
fn p_pos(env: Env) -> game_engine::Point {
todo!("needs implementation")
}
fn p_dir(env: Env) -> game_engine::Direction {
todo!("needs implementation")
}
fn p_points(env: Env) -> u32 {
todo!("needs implementation")
}
fn p_fuel(env: Env) -> u32 {
todo!("needs implementation")
}
fn get_map(env: Env) -> Map<game_engine::Point, game_engine::MapElement> {
todo!("needs implementation")
}
}
This can only be an intermediary step to make sure every function was implemented. Unfortunately only a single #[contractimpl]
is possible per soroban contract.
As we want to augment the logging contract with at least one more function (we need to tell the proxy what to actually proxy, right?) we need to remove to impl Contract for
because we can't add functions to an implementation that are not defined in the trait.
So let's compile the contract to make sure everything is in place:
β― RUSTFLAGS="-A unused" \
cargo build \
--target wasm32-unknown-unknown \
--release
Finished release [unoptimized + debuginfo] target(s) in 0.12s
β―
we are passing the RUSTFLAGS
here once just to prevent β οΈwarningsβ οΈ for all the unused variables
π Great - it builds π οΈ
Proxying the game_engine::Client
Now let's add the actual proxying to the implementation.
First we need a way to call the proxied client. So let's add another function (wrap
) that accepts the client-id.
@@ -1,13 +1,27 @@
-use soroban_sdk::{contractimpl, Env, Map};
+use soroban_sdk::{contractimpl, log, BytesN, Env, Map};
mod game_engine {
soroban_sdk::contractimport!(file = "../game_engine.wasm");
}
-use game_engine::Contract;
+
+const ENGINE_ID: &str = "engine";
pub struct LoggingEngine;
#[contractimpl]
-impl Contract for LoggingEngine {
- fn init(
+impl LoggingEngine {
+ pub fn wrap(env: Env, engine_id: BytesN<32>) {
+ env.storage().set(&ENGINE_ID, &engine_id);
+ log!(&env, "ποΈ logger engine taking notes");
+ }
+
+ fn engine_id(env: Env) -> BytesN<32> {
+ env.storage().get(&ENGINE_ID).unwrap().unwrap()
+ }
+ fn get_engine(env: &Env) -> game_engine::Client {
+ game_engine::Client::new(&env, &Self::engine_id(env.clone()))
+ }
+
+ /// wrapping interface implemention
+ pub fn init(
env: Env,
move_step: u32,
laser_range: u32,
Additionally we'd add all the proxy-calls (see here for the full diff) and build again:
β― cargo build --target wasm32-unknown-unknown --release
Compiling stellar-xdr v0.0.14
Compiling static_assertions v1.1.0
Compiling soroban-env-common v0.0.14
Compiling soroban-env-guest v0.0.14
Compiling soroban-sdk v0.6.0
Compiling logging-engine v0.0.0 (/home/paul/Code/fca00c - logging proxy/contracts/logging_engine)
Finished release [unoptimized + debuginfo] target(s) in 7.91s
β―
π no β οΈwarningβ οΈ anymore
𧩠Now let's put this thing together
Finally we want to see if/how this can be invoked in our test.
It's pretty easy: As both contracts implement the same interface we can even use the original game_engine::Client
to invoke functions in the proxy (logging_engine
).
To prove this is working let's just shoot the first asteroid (which is fortunately just ahead of the starting position):
--- a/contracts/solution/src/lib.rs
+++ b/contracts/solution/src/lib.rs
@@ -18,6 +18,7 @@ impl Solution {
// YOUR CODE START
+ engine.p_shoot();
// YOUR CODE END
}
}
and do this through the logging_engine
:
--- a/contracts/solution/src/test.rs
+++ b/contracts/solution/src/test.rs
@@ -15,7 +15,7 @@
/// cost will decrease as well.
use std::println;
-use soroban_sdk::Env;
+use soroban_sdk::{testutils::Logger, Env};
use crate::{
engine::{Client as GameEngine, WASM as GameEngineWASM},
@@ -24,14 +24,25 @@ use crate::{
extern crate std;
+mod logging_contract {
+ use crate::engine::{Direction, Error, MapElement, Point};
+
+ soroban_sdk::contractimport!(
+ file = "../logging_engine/target/wasm32-unknown-unknown/release/logging_engine.wasm"
+ );
+}
+
/// ESPECIALLY LEAVE THESE TESTS ALONE
#[test]
fn fca00c_fast() {
// Here we install and register the GameEngine contract in a default Soroban
// environment, and build a client that can be used to invoke the contract.
let env = Env::default();
+ let proxy_engine_id = env.register_contract_wasm(None, logging_contract::WASM);
let engine_id = env.register_contract_wasm(None, GameEngineWASM);
- let engine = GameEngine::new(&env, &engine_id);
+ let engine = GameEngine::new(&env, &proxy_engine_id);
+
+ logging_contract::Client::new(&env, &proxy_engine_id).wrap(&engine_id);
// DON'T CHANGE THE FOLLOWING INIT() PARAMETERS
// Once you've submitted your contract on the FCA00C site, we will invoke
@@ -63,12 +74,15 @@ fn fca00c_fast() {
// We reset the budget so you have the best chance to not hit a TrapMemLimitExceeded or TrapCpuLimitExceeded error
env.budget().reset();
- solution.solve(&engine_id);
+ solution.solve(&proxy_engine_id);
+
+ let logs = env.logger().all();
+ println!("{}", logs.join("\n"));
let points = engine.p_points();
println!("Points: {}", points);
- assert!(points >= 100);
+ assert!(points >= 1);
}
#[test]
βnote how the original GameClient
is initialized just with the logging_engine
's idβ
Now run the tests:
β― make test
cargo test fca00c_fast -- --nocapture
Compiling soroban-asteroids-solution v0.0.0 (/home/paul/Code/fca00c - logging proxy/contracts/solution)
Finished test [unoptimized + debuginfo] target(s) in 5.05s
Running unittests src/lib.rs (target/debug/deps/soroban_asteroids_solution-5b0a3423b755223b)
running 1 test
invoker account is not configured
invoker account is not configured
ποΈ logger engine taking notes
Points: 1
test test::fca00c_fast ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.44s
β―
π Great, we've made a proxy contract that allows us to keep the solution clean and does not need us to tinker with the original game_engine
either π!
Top comments (0)