ChainSafe/as-sha256

Remove async initialization

Closed this issue · 6 comments

It would be really nice to provide a seemlessly interoperable interface to most known sha256 implementations but we currently are hindered in that regard by the async initialization currently required by our library. We need this async initialization of our wasm binary because our current build is >4kb, the limit set in Chrome for allowing synchronous initialization.

One avenue to pursue to hide this async initialization is to wait for top-level-await to land in typescript and in ecmascript.

Unfortunately, that seems like a multi-year solution to get into production across the environments we want to support (modern browsers and nodejs).
This technique also cannot hide the async initialization in the creation of multiple hash contexts (something we'd like to allow for to fix our currently async-unsafe API).

An alternative would be to remove the async initialization entirely by reducing our build below 4kb and simply initializing the wasm module synchronously.

I think we can easily get our build size lower than 4kb if we use a non-allocating interface and compile without the AS runtime. The pattern could go something like:

// assemblyscript side
export inputBuffer = new ArrayBuffer(N); // input buffer of length N, write all input here
export outputBuffer = new ArrayBuffer(32);  // output buffer, read hash digest here

export function init(): void {...}
export function update(data: ArrayBuffer, length: number): void {...}
export function update_noalloc(length: number): void {
  update(inputBuffer, length);
}
export function final(): void {
  // write to outputBuffer
}
// typescript side
const module = new WebAssembly.Module(wasmBuf);

export class Sha256 {
  ctx: Instance;
  constructor() {
    this.ctx = loader.instantiateSync(module, {...});
  }
  update(data: Uint8Array) {
    const inputBuffer = this.ctx.__getUint8ArrayView(inputBufferPtr);
    // fill inputBuffer repeatedly until all data is consumed
    for (let i = 0; i < data.length; i += N) { 
      inputBuffer.set(data.slice(i, N));
      this.ctx.update_noalloc(Math.min(data.length - i, N));
    }
  }
  final(): Uint8Array {
    this.ctx.final();
    // copy outputBuffer into new Uint8Array
    const output = new Uint8Array(32);
    const outputBuffer = this.ctx.__getUint8ArrayView(outputBufferPtr);
    output.set(outputBuffer);
    return output;
  }
}

// convenience singleton
const sha256 = new Sha256();

// convenience all-in-one function
export hash(data: Uint8Array): Uint8Array {
  sha256.init();
  sha256.update(data);
  return sha256.final();
}

Thoughts?
@MaxGraey @mpetrunic @GregTheGreek @ec2

You should try latest AS 0.9.1 which introduce drastically smaller runtime for now. Also currently uses -O3 for maximum speed, but you could try -O3z which slowdown your code but could significantly reduce size.

also recommend remove this function and use this helper routine on js host

Thanks for the suggestions.
With AS 0.9.1, -O3z and the function removed, still at 4.1KB build size, see https://github.com/ChainSafe/as-sha256/tree/cayman/reduced-size

I guess reducing 100 bytes will not to be too hard. I'll try reduce this more later

Or try replace all existing memory.fill to "for loops" by yourself with unchecked access to arrays

@MaxGraey curious what your thoughts are on the pattern of copying js buffers into/out of static wasm input/output buffers? Is it generally just more efficient to alloc on the wasm side if we can?

Also curious about if anyref can help us out with this in the future

Also curious about if anyref can help us out with this in the future

I don't think so