l2iterative/bonsai-sdk-php

1.2.0 2024-01-07 10:47 UTC

This package is not auto-updated.

Last update: 2024-04-28 18:39:39 UTC


README

a guy sitting on the subway station waiting for the train to arrive

The current Bonsai API implementation is written in Rust. Since Bonsai requires an API key and would consume the quotas to generate the zero-knowledge proof, web3 applications built with Bonsai would need a backend.

Like most web3 applications, the backend can be minimalistic, because most of the on-chain interactions can be completed through, for example, Metamask's Ethereum provider API, in the frontend.

How would the backend look like?

For the aforementioned reasons, backends often fall into the following six classes:

  • Pure:
    • A pure Typescript implementation, probably with the use of WebAssembly, often implemented through Rust (see wasm-bindgen)
    • A pure PHP implementation
    • A pure Python implementation, such as Django and Flask
  • With foreign function interface (FFI):
    • A Typescript implementation with FFI to high-performance code likely written in Rust, through ffi-rs or koffi
    • A PHP implementation using FFI, probably through the FFI extension
    • A Python implementation using FFI, probably through CFFI

Numerous factors will affect which one to go:

  • Availability of web hosting providers.
    • There are many affordable web hosting platforms for Typescript and PHP.
    • To run Python, the tradition is to use a full-fledged cloud virtual machine (such as an AWS EC2 instance). There is a preference to avoid using a full-fledged VM due to cost and ability to auto-scale.
  • Performance.
    • Everyone would agree that Python is slow.
    • PHP can be optimized through the use of OPcache to precompile the script, but it is not machine code. Typescript can be compiled (together with WebAssembly) into machine code through v8, which we can expect to have the best performance among the "pure" group.
    • Nevertheless, it becomes less relevant when we can use FFI because one can also run machine code, and unlike code generated by just-in-time compilation (JIT), such code is ahead-of-time compilation (AOT), and is excepted to be more performant and should be the go-to whenever available.
  • Developer friendliness.
    • Probably most important: ideally with few learning cycles, no hacking around.
    • Rust is useful here, as RISC Zero programs are now written in Rust, and WebAssembly from Rust has been used in web development, particularly for cryptography.
    • The other programming language is better similar to Rust. Among all the three, PHP is most similar. PHP is known for very friendly to beginners.
    • All there have available Web3 SDK implementation, but Typescript has best support nowadays.

It is important to know that this backend is going to be minimalistic. We have the backend because the API key cannot be exposed to the client. The backend may just consist of:

  • access to a database, such as MySQL, for off-chain data storage
  • integration with a spam prevention system, such as Cloudflare Turnstile or Google reCAPTCHA
  • connecting to RISC Zero Bonsai API, which is the purpose of this repository

How to use

The documentation can be found in the docs folder. It is designed to resemble the Bonsai Rust API here:

https://github.com/risc0/risc0/blob/main/bonsai/sdk/src/alpha.rs

The integration tests can be served as tutorials. It can be as simple as follows:

<?php

use L2Iterative\BonsaiSDK\Client;
use L2Iterative\BonsaiSDK\SessionId;
use L2Iterative\BonsaiSDK\SnarkId;

$client = new Client("https://api.bonsai.xyz", $test_key, $test_version);

$input_id = $client->upload_input($input_data);
$session_id = new SessionId($client->create_session($test_img_id, $input_id, []));

$status = NULL;
while(true) {
    usleep(300);
    $status = $session_id->status($client);
    
    if($status->status != 'RUNNING') {
        break;
    }
}

// error handling if the status is not successful

$snark_id = new SnarkId($client->create_snark($session_id->uuid));

$status = NULL;
while(true) {
    usleep(300);
    $status = $snark_id->status($client);
    
    if($status->status != 'RUNNING') {
        break;
    }
}

// error handling if the status is not successful

// the proof can be forwarded to the frontend to make RPC calls

Serializer

Another challenge when using a different programming language to interact with RISC Zero is about the types. This issue is particularly significant to PHP because PHP does not have separate types for different word sizes.

For example, in PHP, we have int representing integers, nothing else. But this can correspond to i8, i16, i32, i64, u8, u16, u32, u64 in Rust. There are also numbers that PHP cannot easily represent, such as i128 and u128.

Since the Rust type is the missing information here, there is no choice other than letting the developer explicitly provide the Rust type. To do so, the developer is asked to wrap all elements.

Below is a very comprehensive example that can served as a tutorial.

public function test_serialization() {
    $example = new Struct([
        'u8v' => new SameTypeArray([
            new U8(1), new U8(231), new U8(123)
        ]),
        'u16v' => new SameTypeArray([
            new U16(124), new U16(41374)
        ]),
        'u32v' => new SameTypeArray([
            new U32(14710471), new U32(3590275702), new U32(1), new U32(2)
        ]),
        'u64v' => new SameTypeArray([
            new U64(352905235952532), new U64(2147102974910410)
        ]),
        'i8v' => new SameTypeArray([
            new I8(-1), new I8(120), new I8(-22)
        ]),
        'i16v' => new SameTypeArray([
            new I16(-7932)
        ]),
        'i32v' => new SameTypeArray([
            new I32(-4327), new I32(35207277)
        ]),
        'i64v' => new SameTypeArray([
            new I64(-1), new I64(1)
        ]),
        'u8s' => new U8(3),
        'bs' => new Boolean(true),
        'some_s' => new Some(new U16(5)),
        'none_s' => new None(),
        'strings' => new BinaryString("Here is a string."),
        'stringv' => new SameTypeArray([
            new BinaryString("string a"),
            new BinaryString("34720471290497230")
        ])
    ]);

    $serializer = new Serializer();
    $serializer->serialize($example);

    $php_output = $serializer->output();
    $rust_output = [3, 1, 231, 123, 2, 124, 41374, 4, 14710471, 3590275702, 1, 2, 2, 658142100, 82167, 1578999754, 499911, 3, 4294967295, 120, 4294967274, 1, 4294959364, 2, 4294962969, 35207277, 2, 4294967295, 4294967295, 1, 0, 3, 1, 1, 5, 0, 17, 1701995848, 544434464, 1953701985, 1735289202, 46, 2, 8, 1769108595, 1629513582, 17, 842478643, 825701424, 875575602, 858928953, 48];

    $this->assertEquals($php_output, $rust_output);
}

The corresponding Rust struct is defined as follows.

#[derive(Default, Debug, Serialize)]
pub struct SA {
    pub u8v: Vec<u8>,
    pub u16v: Vec<u16>,
    pub u32v: Vec<u32>,
    pub u64v: Vec<u64>,
    pub i8v: Vec<i8>,
    pub i16v: Vec<i16>,
    pub i32v: Vec<i32>,
    pub i64v: Vec<i64>,
    pub u8s: u8,
    pub bs: bool,
    pub some_s: Option<u16>,
    pub none_s: Option<u32>,
    pub strings: String,
    pub stringv: Vec<String>
}

Note that we focus on String here in Rust rather than a binary string because the current serializer does have the trouble to process Vec<u8> in a different manner. If one's Rust code has to use Vec<u8> and cannot afford a wrapper, then the best solution so far is to let PHP break the string down to an array of bytes, through str_split($str).