Implementing a proxy contract for the game_engine to log solution-steps in fca00c
Tags: sorobanathon, soroban, stellar, fca00c
Published: 2023-03-29 22:08:26

To provide a hands on gamified approach to tinker with [**soroban**](soroban.stellar.org/), the latest series of [Stellar Quest](quest.stellar.org) ([fca00c: Asteroids](https://fastcheapandoutofcontrol.com/game/asteroids/play)) was all about submitting a compiled WASM to compete in three categories:

1. earliest working submission
2. smallest working submission
3. 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](https://soroban.stellar.org/docs/how-to-guides/logging#cargotoml-profile)), 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](https://github.com/stellar/fca00c-asteroids/blob/main/contracts/game_engine.wasm) 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](https://soroban.stellar.org/docs/how-to-guides/logging#with-logs)) 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](https://soroban.stellar.org/docs/how-to-guides/logging#using-the-log-macro) 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:

```bash
❯ 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
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=proxy_engine&repo=620754220&machine=basicLinux32gb&location=WestEurope)
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`](https://github.com/hanseartic/fca00c-asteroids/blob/4629981/contracts/logging_engine/src/engine.rs#L9) the complete `game_engine` interface is enforced to be implemented.

`contracts/logging_engine/src/engine.rs:`
{% details show file listing %}
```rust
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")
    }
}
```{% enddetails %}

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:
```bash
❯ 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.

```diff
@@ -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](https://github.com/stellar/fca00c-asteroids/commit/77dc35fc76979113aa6c88c3907f0d3844617ae3) for the full diff) and build again:
```bash
❯ 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):
```diff
--- 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`:
```diff
--- 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**:
```bash
❯ 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 👏!