Skip to content

An experimental Rust SDK and runtime for Tableland Edge Functions

License

MIT, Apache-2.0 licenses found

Licenses found

MIT
LICENSE
Apache-2.0
LICENSE-APACHE
Notifications You must be signed in to change notification settings

sanderpick/tableland-functions

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

45 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tableland-functions

Test License standard-readme compliant

An experimental Rust SDK and runtime for Tableland Edge Functions

Table of Contents

Background

tableland-functions is an experimental Rust SDK and Wasmer runtime for edge function handling on the Tableland network. The architecture is based on cosmwasm and the guest function interface is inspired by Cloudflares's workers-rs.

This repo contains the following crates:

  • tableland_std`: Wasmer compiler, import and export definitions, and type bindings for guest function development.
  • tableland_derive: Macros for guest function development in Rust.
  • tableland_vm: Wasmer host environment and imports API. The current import API provides a function request context with a read method for executing Tableland read-only queries.
  • tableland_worker: HTTP server and Wasmer instance cache. The Worker reponds to evm-tableland events forwarded by a validator, which trigger an instantiation of a WASM binary from IPFS. WASM binaries are compiled, cached, and made available over the Worker's /v1/functions/{wasm_cid} endpoint. See below for a diagram of how this works.
  • tableland_client: A (currently) read-only Tableland client.

POC design

The POC uses IPFS to make the WASM binaries available to validators. However, it is possible to consider using a more resilient layer, such as Filecoin.

tableland-functions

Why?

The Tableland network aims to provide developers with a secure, deterministic, and cloud-like database experience. However, it currently lacks an important component for developers: where to deploy applications or APIs driven by Tableland data. Currently, the available options are limited:

  • IPFS for client-side apps: Requires pinning your application with a pinning service, only works if your application can be built client-side, and does not allow application changes.
  • Use a hosted service like Cloudflare, Supabase, Vercel, etc.: These are not decentralized or deterministic.

The goal of this experiment is to determine whether Tableland is a suitable location for providing a practical solution. This solution should:

  • Enable developers to deploy backends that can query Tableland.
  • Enable users to execute backends from any participating validator.
  • Be scalable or have the potential to scale.
  • Allow validators to measure the amount of work involved in executing queries (this is already necessary for simple read queries).

Tableland Functions

This repository presents a potential solution to the problem described above by adopting the notion of an "edge function". In the context of Tableland, an edge function would provide the following benefits:

  • Reduced latency: By executing functions, which may contain many queries, next to a validator's gateway, the time it takes to process requests can be reduced.
  • Deploy your whole application without a server: Since Tableland is already a cloud-like database, serverless functions would allow developers to skip deploying their own backend, and instead build JSON APIs, render HTML, or even generate SVGs based on Tableland data.
  • Respond with custom HTTP headers.
  • Conditional authorization through signed requests (currently not implemented).

It comes as no surprise that many Database Software as a Service (DB SaaS) offerings have added edge functions that run next to the database.

Requirements

  • Functions should be deterministic. The output should always be the same for a given Tableland network state. This means no float types or external network access.
  • Function execution should be quantifiable. The amount of work required to execute a function should be quantifiable in some unit for a given Tableland network state. Luckily, Wasmer provides a metering feature that is used to track function “gas”. tableland-functions also has a notion of “external gas” (adapted from cosmwasm), which is currently based on query statement and response size (data egress). In the future, the external gas should provide a more accurate representation of the work performed by go-tableland to handle a read query. This might involve measuring statement complexity or simply measuring the time it takes to handle a query.
  • Cold start for functions should be fast. Currently, it takes ~2 seconds, but there is plenty of room for optimization.
  • Functions should execute quickly, and Wasmer is a very fast option. Currently, most of the latency is due to the validator. The example JSON API responds locally in approximately 5-10 milliseconds. This is actually the metric we care about because tableland-functions is intended to be localized with validators.
  • WASM binaries should be relatively small. For example, the JSON API provided here builds to around 180KB. However, you can reduce this to less than 50KB by using custom HTTP types across the WASM bridge, and by using a more constrained JSON serialization library such as serde-json-wasm. Additionally, there is ample opportunity for further optimization, such as compressing the binaries using a tool like UPX.

Edge functions vs. serverless functions

Edge functions are serverless functions that run on the edge, close to your users. Typically, they are part of a Content Delivery Network (CDN) such as Cloudflare, Netlify, Supabase, or Vercel.

In Tableland, database query requests are delivered by validators that may be distributed across the globe. Currently, we do not have latency-based routing for read queries. Once we implement this feature, the entire network will resemble a content delivery network (CDN), although without dynamic content distribution. However, this functionality could be added in the future. Therefore, something like tableland-functions can only be considered edge functions in this future context. For now, they are simply serverless functions.

Comparison to smart contracts

In Tableland, developer-deployed smart contracts allow for on-chain actions that can write Tableland data and for Tableland data to drive on-chain actions via inclusion proofs.

However, smart contracts are not a solution to the problem at hand because they cannot readily respond to HTTP requests. Even if they could, they would be unable to query off-chain data, such as that in Tableland.

Usage

Function development

Currenlty, Rust is the only language you can use to write functions. Future experiments may include AssemblyScript support or JS/TS support using QuickJS (probably via Javy).

Example JSON API

#[entry_point]
pub fn fetch(req: Request, ctx: CtxMut) -> Result<Response> {
    // Optionally, use the Router to handle matching endpoints, use ":name" placeholders, or "*name"
    // catch-alls to match on specific patterns. Alternatively, use `Router::with_data(D)` to
    // provide arbitrary data that will be accessible in each route via the `ctx.data()` method.
    let router = Router::default();

    // Add as many routes as your Function needs! Each route will get a `Request` for handling HTTP
    // functionality and a `RouteContext` which you can use to get route or query parameters.
    router
        .get("/", |_, _, _| Response::ok("Hello from Tableland!"))
        .get("/version", |_, _, _| Response::ok(VERSION))
        .get("/:type", |_, ctx, rctx| {
            if let Some(t) = rctx.param("type") {
                let data = ctx.tableland.read(
                    format!("select * from pets_31337_4 as pets join homes_31337_2 as homes on pets.owner_name = homes.owner_name where type = '{}';", t).as_str(),
                    ReadOptions::default(),
                )?;
                return Response::from_json(&data);
            }
            Response::error("Bad Request", 400)
        })
        .run(req, ctx)
}

This function returns an HTTP response with a JSON payload and headers describing the work performed by the Worker:

curl -v http://localhost:3030/v1/functions/bafkreia4c7orjt23vorxg65vm7b34xvenkgxigsgnmziuhcqp3hi2p5bbi/bird
*   Trying 127.0.0.1:3030...
* Connected to localhost (127.0.0.1) port 3030 (#0)
> GET /v1/functions/bafkreia4c7orjt23vorxg65vm7b34xvenkgxigsgnmziuhcqp3hi2p5bbi/bird HTTP/1.1
> Host: localhost:3030
> User-Agent: curl/7.85.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< content-type: application/json
< x-gas-limit: 500000000000000
< x-gas-remaining: 499707906266700
< x-gas-external: 533300
< x-gas-internal: 292093200000
< content-length: 167
< date: Mon, 06 Mar 2023 23:35:49 GMT
<
* Connection #0 to host localhost left intact
[{"area":"country","name":"Harambe","owner_name":"Dani","type":"bird","value":67000},{"area":"urban","name":"Hodor","owner_name":"Eliza","type":"bird","value":210000}]

See examples for more, including HTML and SVG rendering.

The example tests use mock data. To run the examples in a real Worker, you will need to seed a local go-validator with data:

cd examples/data
./generate.sh

Target wasm32-unknown-unknown to build each example:

cd examples/json
cargo build --target wasm32-unknown-unknown --release

# turn on backtraces to propogate function panics
cargo build --target wasm32-unknown-unknown --release --features=backtraces

# or build a smaller binary (requires nightly toolchain)
cargo +nightly build -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort --target wasm32-unknown-unknown --release

See here for a guide on minimizing the size of binaries. Note, not all recommendations apply to wasm32-unknown-unknown.

While not currently very useful, functions can also respond to POST requests with payloads. This could be helpful in triggering Tableland writes in conjunction with conditional authentication and ERC-4337 account abstraction for gasless transactions.

Run the Worker

cargo run -p tableland_worker

A config file will be written to the expected location for your system. See here for details (specifically, ProjectDirs::config_dir). Below is the default Worker config:

[server]
host = '127.0.0.1'
port = '3030'

[chain]
id = 'Local'

[cache]
directory = '.'

[ipfs]
gateway = 'http://localhost:8081/ipfs'

Development

You will need a local go-tableland validator and a local EVM node running the TablelandTables contract from evm-tableland. The easiest way to do this is with local-tableland. However, tableland-functions requires specific branches of go-tableland and evm-tableland (see here and here. You may find it easier to spin the components manually.

Contributing

PRs accepted.

Small note: If editing the README, please conform to the standard-readme specification.

License

MIT AND Apache-2.0, © 2021-2022 Tableland Network Contributors

About

An experimental Rust SDK and runtime for Tableland Edge Functions

Resources

License

MIT, Apache-2.0 licenses found

Licenses found

MIT
LICENSE
Apache-2.0
LICENSE-APACHE

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages