Exploring alternatives to WASM for smart contracts

I just tried with rustc 1.71.0. This is the manifest:

[package]
name = "risky"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

Then I do cargo build --target riscv32i-unknown-none-elf and get this error:

error: cannot produce cdylib for `risky v0.1.0` as the target `riscv32i-unknown-none-elf` does not support these crate types

@Alex Ah, okay, sorry, you’re right! I got confused because my test program is both a bin crate and a cdylib crate.

But this doesn’t mean it must have a single entry point. (: You can add a #![no_main] attribute at the top of your crate and then you don’t need a main and can export multiple entry points.

src/main.rs:

#![no_std]
#![no_main]

// To make sure the program is recompiled if the linker script changes.
const _: &[u8] = include_bytes!("memory.ld");

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

#[no_mangle]
pub extern fn add_u32(a: u32, b: u32) -> u32 {
    a + b
}

#[no_mangle]
pub extern fn mul_u32(a: u32, b: u32) -> u32 {
    a * b
}

src/memory.ld:

SECTIONS {
    . = 0x10000;
    .rodata : { *(.rodata) *(.rodata.*) }

    . = ALIGN(0x4000);
    .data : { *(.sdata) *(.data) }
    .bss : { *(.sbss) *(.bss) *(.bss.*) }

    . = 0xf0000000;
    .text : { KEEP(*(.text .text.*)) }
}

Result

RUSTFLAGS="-C link-arg=-Tsrc/memory.ld -C target-feature=+m" cargo build --target riscv32i-unknown-none-elf --release && riscv32-elf-objdump -d target/riscv32i-unknown-none-elf/release/tst && readelf -sW target/riscv32i-unknown-none-elf/release/tst
   Compiling tst v0.1.0 (/tmp/tst)
    Finished release [optimized] target(s) in 0.07s

target/riscv32i-unknown-none-elf/release/tst:     file format elf32-littleriscv


Disassembly of section .text:

f0000000 <rust_begin_unwind>:
f0000000:	0000006f          	j	f0000000 <rust_begin_unwind>

f0000004 <add_u32>:
f0000004:	00a58533          	add	a0,a1,a0
f0000008:	00008067          	ret

f000000c <mul_u32>:
f000000c:	02a58533          	mul	a0,a1,a0
f0000010:	00008067          	ret

Symbol table '.symtab' contains 5 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS tst.452cca67637c277d-cgu.0
     2: f0000000     4 FUNC    LOCAL  HIDDEN     1 rust_begin_unwind
     3: f0000004     8 FUNC    GLOBAL DEFAULT    1 add_u32
     4: f000000c     8 FUNC    GLOBAL DEFAULT    1 mul_u32

You are right. I am using no_main anyways as it does not really make sense bare metal. So yeah having multiple entry points for contracts is viable using the bin target. But I really don’t want to make pallet-contracts parse more metadata. Just accepting any address as entry point is unsafe because we only want to allow to call into pre-defined entry points (messages in ink!). So we need to include a list of allowed entry points for deploy and call in our binary format.

Any thoughts on dynamic linking? There is risc-v and even wasm tooling for dynamic linking, but could one “push dynamic linking through” this conversion, so then your final artifact winds up being dynamically linked by dlopen or similar?

At least for runtimes, we could then have a family or artifacts on the relay chain, of which one could update only some of them. Among these, we could’ve large SRSes used for proof systems. We could’ve a separate interface for those, or even put some into the host, but realistically the optimal interface might resemble dlopen.

It’s plausible this benefits contracts somehow too, like by making the contract smaller, assuming you use enough dyn Trait or whatever.

Yes. I’ve discussed this privately with Alex already, and he also expressed a sentiment that it’d be useful for smart contracts.

It’ll require emitting relocation information inside of the program blob in some way and we’ll have to make sure the baseline design of the VM is compatible with it, but in general it should be doable. Functionally the rough idea here is that that a program can import and export multiple functions, and then the imports can be either fulfilled by the host (a host function) or by another program (an exported function).

I probably won’t add this until v2.0 or something (at least not in a production-ready fashion), but I’d like to get the design of the VM solidified enough before v1.0 so that adding support for linking multiple modules together won’t be too much trouble. (The assumption here is, of course, that all of the things we want for standalone programs - like fast O(n) compilation and full determinism - will also have to hold for multi-module program.)

1 Like

RE: Dynamic linking; I also think it’d be useful for contracts. Libraries are heavily used in contracts. However right now the options are: Either link everything statically or treat external contracts as library. Former is bad regarding code size, also the whole code has to be loaded regardless whether it’s used during a call or not. Latter is very costly as every call into a library function requires an external call, ABI encoding the arguments and ABI decoding the return values. So now it’s more like a pick your poison situation.

No. We do need cross VM calls for the security model of SPREEs, but that’s not dynamic linking. There is no encoding & decoding in dynamic linking. There maybe indirection in dynamic linking, but the overhead is minimal.

FYI, continuing this work I posted a fresh new topic with an update: Announcing PolkaVM - a new RISC-V based VM for smart contracts (and possibly more!)

We can now consider this thread closed.