left-curve / grug

Grug
https://leftcurve.software/grug
71 stars 9 forks source link

Code pinning #57

Closed larry0x closed 1 month ago

larry0x commented 1 month ago

Wasmer provides three compilers that convert WebAssembly bytecodes to native machine codes: Singlepass, Cranelift, and LLVM.

Singlepass is the fastest at compile time but slowest at run time (up to 8x slower than native Rust).

LLVM is the slowest at compile time by a big margin (>150x slower than Singlepass) but fastest at run time (<2x slower than native Rust).

(Benchmark)

For Grug, there is benefit in compiling certain "privileged contracts" (e.g. bank, taxman, some basic accounts, IBC clients) using LLVM, but other contracts are better using Singlepass.

Suggested implementation

Add a prinned_hashes field to Config:

struct Config {
    pub owner: Addr,
    pub bank: Addr,
    pub taxman: Addr,
    pub pinned_hashes: BTreeSet<Hash>,
    // ...
}

Add a pinned_modules field to the Wasm VM cache:

struct WasmCacheInner {
    modules: LruCache<Hash, (Module, Engine)>,
    // new!
    // Use a `HashMap` instead of `LruCache` because we don't need
    // an upper bound for this cache's size, since pinned modules
    // are set by governance.
    pinned_module: HashMap<Hash, (Module, Engine)>,

}

struct WasmCache {
    inner: Arc<RwLock<WasmCacheInner>>,
}

When creating the App<DB, VM>, the VM is provided with the list of hashes and codes to be pinned. At this time, it should build the pinned contracts using LLVM compiler at this time:

trait Vm {
    // new method
    fn new(pinned_hashes: BTreeSet<Hash>) -> VmResult<Self>;
}

impl<DB, VM> App<DB, VM>
where
    DB: Db,
    Vm: VM,
{
    pub fn new(db: DB) -> AppResult<Self> {
        // TODO: need to handle the case that prior to genesis,
        // the config hasn't been saved to the DB yet.
        let cfg = CONFIG.load(db.state_storage(None))?;

        // Owner, bank, taxman, etc. are always pinned,
        // regardless whether they're in the pinned hashes list.
        let mut pinned_hashes = cfg.pinned_hashes.clone();
        pinned_hashes.insert(cfg.owner.clone());
        pinned_hashes.insert(cfg.bank.clone());
        pinned_hashes.insert(cfg.taxman.clone());

        let vm = VM::new(pinned_hashes)?;

        Ok(Self { db, vm })
    }
}
larry0x commented 1 month ago

We realized that the LLVM compiler accesses an external shared object (libllvm-*.so) through the C API. This means anything involving it will have trouble being SNARK proved using a zkVM.

Provable computation is important for us. Therefore we can't include the LLVM compiler.

Without LLVM compiler, pinning codes is kinda meaningless as well, since privileged contracts like owner, bank, taxman probably will be constantly cached in the LRU cache anyways.