# Motoko compiler plugins for fast computation

**URL:** <https://forum.dfinity.org/t/motoko-compiler-plugins-for-fast-computation/76041>\
**Category:** Developers\
**Created:** [October 11, 2026, 10:51am UTC](https://forum.dfinity.org/t/motoko-compiler-plugins-for-fast-computation/76041 "2026-10-11T10:51:14Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![infu](https://sea1.discourse-cdn.com/flex023/user_avatar/forum.dfinity.org/infu/32/11321_2.png) [@infu](https://forum.dfinity.org/u/infu)\
**Post date:** [October 11, 2026, 10:51am UTC](https://forum.dfinity.org/t/motoko-compiler-plugins-for-fast-computation/76041/1 "2026-10-11T10:51:14Z")

</div>

AI makes it easy to write small plugins/components and test them well, we should use that. It’s much worse at building bigger things. It feels like it’s compiler hack season and as much as I like modding mine with AI, I’d prefer having the people who made Motoko maintain the core with their human brains and let us hack smaller things we can test and understand.

Let all credits for this plugin system go to whoever manages to review it and consider its broader implications. I merely wrote few prompts to an agent to get this PR.

[https://github.com/caffeinelabs/motoko/pull/6443](https://github.com/caffeinelabs/motoko/pull/6443)

* * *

We’ve built an experimental compute-plugin system for native `moc`. It lets developers distribute optimized functions as separate packages and expose them through Motoko’s `Prim` module. The first two plugins provide **canonical Candid encoding** and **SHA-256**.

The focus is pure, synchronous computation. CandidX generates an encoder specialized for the actual Motoko types, while SHA-256 reads the existing input Blob directly.

With our plugin-enabled compiler, load prebuilt packages dynamically when compiling a canister:

```sh
moc --plugin ./packages/candidx \
    --plugin ./packages/sha256 \
    Main.mo -o Main.wasm

```

Motoko code can then use:

```motoko
import Prim "mo:⛔";

let bytes = Prim.to_candid_canonical({ id = 42; name = "Alice" });
let digest = Prim.sha256(bytes);

```

Adding a compatible plugin does not require rebuilding `moc`. The compiler links the required kernels into one ordinary canister Wasm module, with direct calls. Packages are trusted extensions and must match the compiler/toolchain; incompatible packages produce an “unsupported compute plugin” error.

Representative **PocketIC instruction counts** , using the same compiler and canister for both implementations:

| Workload | Motoko library | Plugin | Library / plugin |
| --- | --- | --- | --- |
| SHA-256, 32 bytes | 27,968 | 4,966 | 5.63× |
| SHA-256, 1 MiB | 323,721,060 | 78,124,848 | 4.14× |
| CandidX, 64 KiB Blob | 210,032 | 66,322 | 3.17× |
| CandidX, 1,024 fixed records | 37,743,426 | 351,914 | 107.25× |
| CandidX, 1,024 sparse records | 88,379,476 | 5,674,118 | 15.58× |
| CandidX, 1,024 distinct variant tags | 1,428,890,507 | 2,190,869 | 652.20× |
| CandidX, 1,024 variants repeating one tag | 23,692,669 | 775,646 | 30.55× |

**How does this compare with well-optimized Wasm components?** Components provide standard interfaces and isolation. An optimized Motoko integration could use the same kernels, generate specialized bindings, and minimize adapter overhead. The components can have transfers down to a single copy between the two memories. Our plugins avoid that boundary transfer by sharing Motoko’s memory.

Native Component Model execution would also need IC support and are not allowed now.

For our functions:

- SHA-256: a separate component normally receives a copy of the input bytes. Our plugin reads the existing Blob directly. The copy’s cost could still be small relative to hashing.
- A standalone CandidX encoder component: normally needs the input converted into its representation, then the encoded bytes transferred back. Our implementation reads Motoko values and writes the final Motoko Blob directly.

---

<div class="post-metadata">

**Author:** ![infu](https://sea1.discourse-cdn.com/flex023/user_avatar/forum.dfinity.org/infu/32/11321_2.png) [@infu](https://forum.dfinity.org/u/infu)\
**Post date:** [October 11, 2026, 5:33pm UTC](https://forum.dfinity.org/t/motoko-compiler-plugins-for-fast-computation/76041/2 "2026-10-11T17:33:27Z")

</div>

# Motoko plugins: instruction costs and allocation in practice

We benchmarked six experimental Motoko compiler plugins: JSON, CBOR, CandidX,  
SHA-256, a persistent map and a separate mutable map. They load during canister  
compilation, expose Motoko functions, and specialize operations on typed values  
through the compiler’s GC helpers.

Tables show PocketIC instruction counts from three trials. Each library/plugin  
pair uses the same compiler and inputs; compiler versions differ across recorded  
rounds. The ratio is **library instructions / plugin instructions**. These are  
instruction savings, not wall-clock speed measurements.

| Workload | Motoko baseline | Library instructions | Plugin instructions | Ratio |
| --- | --- | --- | --- | --- |
| JSON encode, 1,024 records | `json 1.4.0` | 53,258,267 | 5,044,780 | 10.56× |
| JSON decode, 1,024 records | `serde 3.5.0` | 763,222,636 | 18,210,769 | 41.91× |
| CBOR encode, 1,024 records | `cbor 4.1.0` | 49,934,053 | 5,395,241 | 9.26× |
| CBOR decode, 1,024 records | `cbor 4.1.0` | 152,527,112 | 7,324,274 | 20.82× |
| Canonical CandidX encode, 1,024 sparse records | CandidX Motoko library, `94c2868` | 88,379,476 | 6,057,260 | 14.59× |
| SHA-256, 1 MiB | `sha2 0.2.5` | 323,721,060 | 78,124,848 | 4.14× |

For JSON and CBOR, each row uses the lowest instruction count among the three  
tested native libraries. These are end-to-end typed operations, including  
conversion to and from each library’s representation. Typed traversal and  
avoiding intermediate representations are part of the benefit.

Allocation reductions are substantial too. JSON decoding above drops from  
**136,203,896 to 172,816 allocated bytes** ; CandidX encoding drops from  
**3,578,064 to 22,600 bytes**. These counters include results and temporary  
garbage. They measure allocation pressure, **not peak or retained live memory**.

Both map plugins support generic keys and values. The persistent map shares  
unchanged branches between versions; the mutable map updates nodes in place.  
Neither copies an entire map on an ordinary point update.

The following comparisons use **100,000-entry maps** , boxed `Nat64` keys and  
the same ordinary Motoko comparator in both implementations. Each plugin is  
compared with its corresponding `core 2.5.0` map. Counts are per-call averages  
over batches of 32 operations.

| Map | Operation | Core instructions | Plugin instructions | Ratio | Allocated bytes, Core → plugin |
| --- | --- | --- | --- | --- | --- |
| Persistent | Lookup, hit | 3,324.09 | 2,524.38 | 1.32× | 0 → 0 |
| Persistent | Insert new key | 14,112.34 | 10,148.47 | 1.39× | 2,524.25 → 1,289.25 |
| Persistent | Overwrite | 12,817.72 | 6,066.84 | 2.11× | 2,230.50 → 1,143 |
| Mutable | Lookup, hit | 7,700.16 | 3,877.41 | 1.99× | 123 → 0 |
| Mutable | Insert new key | 12,818.28 | 5,499.44 | 2.33× | 324 → 40 |
| Mutable | Overwrite | 8,528.06 | 4,492.91 | 1.90× | 286 → 40 |
| Mutable | Delete, hit | 14,111.88 | 7,835.81 | 1.80× | 520.25 → 0 |

Optional scalar ordering removes comparator callbacks while keeping values  
generic. At this size, `getNat64` uses **1,345.19 instructions** for the  
persistent plugin and **2,755.84** for the mutable plugin. Generic values can  
include shared records, closures and large Blobs; payloads retain their identity.

Whole-call costs at 100,000 entries:

| Mutable map, 100,000 entries | Core instructions | Plugin instructions | Ratio | Allocated bytes, Core → plugin |
| --- | --- | --- | --- | --- |
| Build from shuffled entries | 1,175,071,733 | 566,138,242 | 2.08× | 30,051,776 → 5,842,360 |
| Predicate search, no match | 170,277,593 | 40,524,835 | 4.20× | 11,059,840 → 72 |

Shuffled construction inserts the same entries in a fixed permutation. Predicate  
search visits the whole map in key order; Core’s baseline uses an iterator  
adapter because it has no matching `find` function. These comparisons include  
algorithm and representation choices, as well as generated code.

There are tradeoffs. The mutable plugin’s 100,000-entry `map` transform uses  
**26.7% more instructions** than Core, although it allocates less. Its full  
138-workload matrix has 135 instruction wins, two ties and one loss; `size`  
and `isEmpty` are effectively at parity. Scalar ordering also loses to the  
callback path in some removal cases. For the persistent plugin, creating a  
cursor or snapshot costs more, and shuffled `fromIter` construction is slightly  
slower at 1,000 and 10,000 entries.

Input preparation, correctness checks, reply encoding and message-end GC are  
outside timing. Less allocation is encouraging for GC pressure, but these  
measurements do not establish a steady-state GC or whole-canister speedup.  
The implementations have separate correctness audits, including interpreter/Wasm  
agreement, shared references, GC and upgrade tests for the mutable map.

These results support compiler plugins for expensive operations on typed Motoko  
data. Developers load only the packages they need, with improvements checked  
against real library implementations.

**[Full benchmark tables, methodology and audit links are in the README.](https://github.com/Neutrinomic/motoko_plugins/blob/main/README.md)**

---

<div class="post-metadata">

**Author:** ![timo](https://sea1.discourse-cdn.com/flex023/user_avatar/forum.dfinity.org/timo/32/5543_2.png) [@timo](https://forum.dfinity.org/u/timo)\
**Post date:** [October 12, 2026, 9:13am UTC](https://forum.dfinity.org/t/motoko-compiler-plugins-for-fast-computation/76041/3 "2026-10-12T09:13:04Z")

</div>

Interested to see how cryptographic operations like EC stuff perform as plugins. And then there are some things we don’t even in Motoko right now like BLS curves.

---

<div class="post-metadata">

**Author:** ![infu](https://sea1.discourse-cdn.com/flex023/user_avatar/forum.dfinity.org/infu/32/11321_2.png) [@infu](https://forum.dfinity.org/u/infu)\
**Post date:** [October 12, 2026, 1:44pm UTC](https://forum.dfinity.org/t/motoko-compiler-plugins-for-fast-computation/76041/4 "2026-10-12T13:44:37Z")

</div>

# Crypto public API benchmarks

155 public callables. **Native** means pure Motoko from pinned Mops packages; **Plugin** means the optimized `mo:fast` implementation. `Native/plugin` is the measured instruction ratio (larger favors the plugin). Empty native cells mean no correct counterpart for the listed operation in the pinned packages. Marked rows use the closest available code with the contract differences explained below.

Each value is the median of three fresh-canister trials. Inputs and reusable setup are prepared before measurement; only the named operation and result assignment are timed. Exact independent output checks run afterward. Instructions are IC instructions/op, including measured call overhead; allocated bytes are cumulative allocation/op, not live heap. Whole-canister Wasm sizes, all workload sizes, hashes and individual trials are retained in the machine-readable evidence.

Measured with Motoko compiler (source 2.0.0-14-g92b1bdecb), pocket-ic-server 10.0.0, and release RTS SHA256 `8456e47e527bbd58…` (full hash in evidence).

Pinned packages: `sha2@0.1.14`, `blake3@0.1.2`, `ecdsa@8.0.1`, `bitcoin@0.2.1`, `bls12-381@0.2.0`. Byte-oriented curve adapters include parsing, validation and serialization except where marked; reusable curve construction is outside timing. Prepared and streaming rows exclude construction unless construction is the named operation. Every counterpart and any contract differences are recorded per row in the evidence file.

## BLAKE3

| Public function | Workload | Size | Native instructions/op | Plugin instructions/op | Native allocated B/op | Plugin allocated B/op | Native/plugin |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `deriveKey` [B] | isolated deriveKey | 1024 bytes | 3,875,001 | 38,805 | 176,104 | 56 | 99.86× |
| `digest` | unkeyed digest | 1024 bytes | 3,485,872 | 35,745 | 135,536 | 56 | 97.52× |
| `finalize` | isolated finalize | 1024 bytes | 265,269 | 41,754 | 24,304 | 88 | 6.35× |
| `finalizeXof` | short seeked XOF within one output block | 1024-byte input; seek 13; output 32 bytes | 267,446 | 41,750 | 24,312 | 88 | 6.41× |
| `keyedHash` | keyed digest | 32-byte key; 1024 bytes | 3,492,129 | 35,935 | 135,944 | 88 | 97.18× |
| `new` | isolated new | empty state | 92,375 | 6,406 | 14,768 | 2,840 | 14.42× |
| `newDeriveKey` [B] | isolated newDeriveKey | empty state | 481,567 | 9,579 | 55,336 | 2,840 | 50.27× |
| `newKeyed` | isolated newKeyed | 32-byte key; empty state | 98,592 | 6,691 | 15,176 | 2,872 | 14.74× |
| `update` | isolated update | 1024 bytes | 3,128,677 | 19,584 | 96,464 | 2,872 | 159.76× |
| `xof` | short seeked XOF within one output block | 1024-byte input; seek 13; output 32 bytes | 3,487,986 | 36,678 | 135,544 | 88 | 95.10× |

[B] Native composes published state/flag operations for correct domain-separated derivation. Extra long-XOF workloads compose output-block calls. These bounded cases match official vectors; the package’s high-level derive function ignores context, and its multi-chunk hashing remains incorrect.

## SHA512

| Public function | Workload | Size | Native instructions/op | Plugin instructions/op | Native allocated B/op | Plugin allocated B/op | Native/plugin |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `Digest` | constructor | 0 B | 9,529 | 1,430 | 3,256 | 432 | 6.66× |
| `Digest.reset` | reset | 1024 B | 1,194 | 718 | 0 | 240 | 1.66× |
| `Digest.sum` | sum | 1024 B | 61,335 | 9,451 | 5,736 | 88 | 6.49× |
| `Digest.write` | write | 1024 B | 154,080 | 56,112 | 256 | 240 | 2.75× |
| `Digest384` | constructor | 0 B | 9,274 | 1,430 | 3,128 | 432 | 6.49× |
| `Digest384.reset` | reset | 1024 B | 1,194 | 718 | 0 | 240 | 1.66× |
| `Digest384.sum` | sum | 1024 B | 56,677 | 9,348 | 5,304 | 72 | 6.06× |
| `Digest384.write` | write | 1024 B | 154,080 | 56,114 | 256 | 240 | 2.75× |
| `digest` | one-shot | 1024 B | 182,086 | 61,018 | 4,544 | 88 | 2.98× |
| `digest384` | one-shot | 1024 B | 180,772 | 60,917 | 4,240 | 72 | 2.97× |

## RIPEMD160

| Public function | Workload | Size | Native instructions/op | Plugin instructions/op | Native allocated B/op | Plugin allocated B/op | Native/plugin |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `Digest` | constructor | 0 B | 2,807 | 1,322 | 704 | 320 | 2.12× |
| `Digest.reset` | reset | 1024 B | 542 | 610 | 0 | 128 | 0.89× |
| `Digest.sum` [M] | sum | 1024 B | 40,253 | 5,738 | 232 | 48 | 7.02× |
| `Digest.write` | write | 1024 B | 527,428 | 55,496 | 8,216 | 128 | 9.50× |
| `digest` | one-shot | 1024 B | 570,076 | 57,247 | 9,152 | 48 | 9.96× |
| `finalize` [M] | immutable-state-finalize | 1024 B | 40,253 | 5,678 | 232 | 48 | 7.09× |
| `init` [M] | immutable-state-init | 0 B | 2,807 | 577 | 704 | 128 | 4.86× |
| `update` [M] | immutable-state-update | 1024 B | 541,428 | 55,634 | 8,216 | 128 | 9.73× |

[M] Native constructs or mutates an object and consumes it on finalization. Plugin state is an immutable serialized snapshot, and finalization preserves it. Ratios include these different state contracts.

## HMAC

| Public function | Workload | Size | Native instructions/op | Plugin instructions/op | Native allocated B/op | Plugin allocated B/op | Native/plugin |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `SHA256` | constructor | 32-byte key; empty stream | 118,338 | 12,527 | 13,808 | 408 | 9.45× |
| `SHA256.reset` | reset | 32-byte key; 1024-byte message | 9,658 | 273 | 0 | 0 | 35.38× |
| `SHA256.sum` | sum | 32-byte key; 1024-byte message | 92,529 | 11,332 | 8,328 | 56 | 8.17× |
| `SHA256.write` | write | 32-byte key; 1024-byte message | 318,024 | 79,174 | 0 | 160 | 4.02× |
| `SHA512` | constructor | 32-byte key; empty stream | 159,435 | 17,349 | 16,288 | 544 | 9.19× |
| `SHA512.reset` | reset | 32-byte key; 1024-byte message | 18,934 | 273 | 0 | 0 | 69.36× |
| `SHA512.sum` | sum | 32-byte key; 1024-byte message | 116,629 | 16,384 | 10,536 | 88 | 7.12× |
| `SHA512.write` | write | 32-byte key; 1024-byte message | 154,145 | 56,141 | 256 | 296 | 2.75× |
| `prepareSha256` | prepare | 32-byte key | 98,915 | 11,948 | 10,264 | 280 | 8.28× |
| `prepareSha256.digest` | prepared digest | 32-byte key; 1024-byte message | 402,905 | 89,322 | 7,424 | 56 | 4.51× |
| `prepareSha256.stream` | prepared stream | 32-byte key; empty stream | 19,679 | 1,045 | 3,544 | 200 | 18.83× |
| `prepareSha256.stream.reset` | prepared reset | 32-byte key; 1024-byte message | 9,658 | 273 | 0 | 0 | 35.38× |
| `prepareSha256.stream.sum` | prepared sum | 32-byte key; 1024-byte message | 92,529 | 11,332 | 8,328 | 56 | 8.17× |
| `prepareSha256.stream.write` | prepared write | 32-byte key; 1024-byte message | 318,024 | 79,174 | 0 | 160 | 4.02× |
| `prepareSha512` | prepare | 32-byte key | 130,659 | 16,770 | 12,824 | 416 | 7.79× |
| `prepareSha512.digest` | prepared digest | 32-byte key; 1024-byte message | 256,072 | 69,813 | 9,344 | 88 | 3.67× |
| `prepareSha512.stream` | prepared stream | 32-byte key; empty stream | 29,032 | 1,045 | 3,464 | 200 | 27.78× |
| `prepareSha512.stream.reset` | prepared reset | 32-byte key; 1024-byte message | 18,934 | 273 | 0 | 0 | 69.36× |
| `prepareSha512.stream.sum` | prepared sum | 32-byte key; 1024-byte message | 116,629 | 16,384 | 10,536 | 88 | 7.12× |
| `prepareSha512.stream.write` | prepared write | 32-byte key; 1024-byte message | 154,145 | 56,141 | 256 | 296 | 2.75× |
| `sha256` | authenticate one message | 32-byte key; 1024-byte message | 492,476 | 99,364 | 18,112 | 56 | 4.96× |
| `sha512` | authenticate one message | 32-byte key; 1024-byte message | 350,173 | 83,009 | 22,304 | 88 | 4.22× |

## Base58

| Public function | Workload | Size | Native instructions/op | Plugin instructions/op | Native allocated B/op | Plugin allocated B/op | Native/plugin |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `decode` | Bitcoin Base58 decode | 111-character input → 82-byte output | 187,866 | 13,747 | 1,544 | 280 | 13.67× |
| `encode` | Bitcoin Base58 encode | 82-byte input → 111-character output | 228,644 | 16,662 | 2,864 | 280 | 13.72× |

## Base58Check

| Public function | Workload | Size | Native instructions/op | Plugin instructions/op | Native allocated B/op | Plugin allocated B/op | Native/plugin |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `decode` | checksummed Bitcoin extended-key sized payload | 78-byte payload; 4-byte checksum | 287,170 | 40,525 | 10,032 | 1,216 | 7.09× |
| `encode` | checksummed Bitcoin extended-key sized payload | 78-byte payload; 4-byte checksum | 346,092 | 46,716 | 12,040 | 1,240 | 7.41× |

## Bech32

| Public function | Workload | Size | Native instructions/op | Plugin instructions/op | Native allocated B/op | Plugin allocated B/op | Native/plugin |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `convertBits` [E] | 8-bit to padded 5-bit groups | 32 input bytes; 52 output groups | 23,292 | 1,656 | 1,312 | 112 | 14.07× |
| `decode` | Bech32m decode | 2-byte HRP; 53 groups | 78,834 | 9,769 | 3,688 | 344 | 8.07× |
| `encode` | Bech32m encode | 2-byte HRP; 53 groups | 69,729 | 5,214 | 3,088 | 240 | 13.37× |

[E] Native uses the exact attributed private `bitcoin/Segwit.convertBits` function, extracted into the benchmark. It assumes valid bit widths; the plugin validates widths and enforces size limits. Both perform the same valid conversion.

## Segwit

| Public function | Workload | Size | Native instructions/op | Plugin instructions/op | Native allocated B/op | Plugin allocated B/op | Native/plugin |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `decode` | Taproot v1 witness program decode | 32-byte program; 62-character address | 95,596 | 24,445 | 4,400 | 1,560 | 3.91× |
| `encode` | Taproot v1 witness program encode | 32-byte program; 62-character address | 201,151 | 21,175 | 8,968 | 1,408 | 9.50× |

## Secp256k1

| Public function | Workload | Size | Native instructions/op | Plugin instructions/op | Native allocated B/op | Plugin allocated B/op | Native/plugin |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `baseMultiply` | Generator times full-width scalar | 32 bytes | 124,175,848 | 6,351,461 | 5,444,616 | 96 | 19.55× |
| `compressPublicKey` | Valid uncompressed SEC1 input | 65 bytes | 9,341,246 | 12,736 | 470,584 | 96 | 733.45× |
| `convertPublicKey` | Valid compressed SEC1 to uncompressed | 33 bytes | 39,574,794 | 575,531 | 1,664,968 | 128 | 68.76× |
| `decompressPublicKey` | Valid compressed SEC1 input | 33 bytes | 39,574,794 | 575,516 | 1,664,968 | 128 | 68.76× |
| `derivePublicKey` | 32-byte secret; compressed SEC1 output | 32 bytes | 63,224,878 | 6,351,398 | 5,278,944 | 96 | 9.95× |
| `normalizeSignature` | Compact high-S to low-S signature | 64 bytes | 14,488,926 | 2,584 | 666,624 | 120 | 5607.17× |
| `pointAdd` | Compressed and uncompressed distinct points | 33 + 65 bytes | 28,485,267 | 1,197,611 | 1,248,456 | 96 | 23.79× |
| `pointMultiply` | Compressed point and full-width scalar | 33 + 32 bytes | 148,898,913 | 6,922,349 | 6,414,664 | 96 | 21.51× |
| `publicKey` | Full-width secret; uncompressed SEC1 output | 32 bytes | 70,236,803 | 6,926,708 | 5,596,840 | 224 | 10.14× |
| `schnorrSign` | Official BIP340 vector 1, supplied auxiliary bytes | 32-byte message | | 12,762,901 | | 120 | |
| `schnorrVerify` | Official BIP340 vector 1, valid signature | 32-byte message + 64-byte signature | | 9,920,568 | | 56 | |
| `signPrehash` [N] | RFC6979 deterministic ECDSA of a SHA-256 digest | 32-byte secret + 32-byte digest | 76,010,180 | 7,452,214 | 6,002,504 | 120 | 10.20× |
| `signatureFromDer` | Minimal DER to compact signature | 71 bytes | 16,972,885 | 4,530 | 822,160 | 120 | 3746.77× |
| `signatureToDer` | Compact low-S signature to minimal DER | 64 bytes | 14,826,684 | 6,606 | 677,040 | 224 | 2244.43× |
| `tweakPublicKey` | Compressed point plus full-width scalar times generator | 33 + 32 bytes | 149,275,314 | 6,957,310 | 6,440,800 | 96 | 21.46× |
| `tweakXOnlyPublicKey` | Even-Y lift of X-only key plus full-width tweak | 32 + 32 bytes | 152,652,841 | 6,957,132 | 6,684,456 | 96 | 21.94× |
| `tweakXOnlySecretKey` | Full-width even-Y secret and tweak | 32 + 32 bytes | 124,045,300 | 6,349,356 | 5,446,472 | 88 | 19.54× |
| `verifyPrehash` | Valid compact low-S signature and compressed public key | 33 + 32 + 64 bytes | 135,628,126 | 10,093,580 | 10,763,472 | 56 | 13.44× |
| `xOnlyPublicKey` | Validate compressed SEC1 and extract X | 33 bytes | 24,765,721 | 571,759 | 980,168 | 88 | 43.31× |

[N] Native signs with an independently prepared RFC6979 nonce; plugin derives that nonce inside the measured call. Native secret arithmetic is variable-time. This is a closest-operation comparison, not equal API or side-channel guarantees.

## P256

| Public function | Workload | Size | Native instructions/op | Plugin instructions/op | Native allocated B/op | Plugin allocated B/op | Native/plugin |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `convertPublicKey` | Valid compressed SEC1 to uncompressed | 33 bytes | 31,098,672 | 581,934 | 1,397,304 | 128 | 53.44× |
| `derivePublicKey` | 32-byte secret; compressed SEC1 output | 32 bytes | 168,233,483 | 5,207,095 | 12,489,816 | 96 | 32.31× |
| `normalizeSignature` | Compact high-S to low-S signature | 64 bytes | 14,437,561 | 2,138 | 664,552 | 120 | 6752.83× |
| `publicKey` | Full-width secret; uncompressed SEC1 output | 32 bytes | 175,332,443 | 5,207,302 | 12,810,992 | 128 | 33.67× |
| `signPrehash` [N] | RFC6979 deterministic ECDSA of a SHA-256 digest | 32-byte secret + 32-byte digest | 182,163,354 | 13,458,880 | 13,290,216 | 120 | 13.53× |
| `signatureFromDer` | Minimal DER to compact signature | 70 bytes | 16,875,526 | 4,196 | 817,952 | 120 | 4021.81× |
| `signatureToDer` | Compact low-S signature to minimal DER | 64 bytes | 14,518,740 | 6,299 | 667,264 | 224 | 2304.93× |
| `verifyPrehash` | Valid compact low-S signature and compressed public key | 33 + 32 + 64 bytes | 341,837,259 | 12,873,707 | 25,290,680 | 32 | 26.55× |

[N] Native signs with an independently prepared RFC6979 nonce; plugin derives that nonce inside the measured call. Native secret arithmetic is variable-time. This is a closest-operation comparison, not equal API or side-channel guarantees.

## BLS12381

| Public function | Workload | Size | Native instructions/op | Plugin instructions/op | Native allocated B/op | Plugin allocated B/op | Native/plugin |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `aggregateVerifyEthereum` | Verify Ethereum aggregate, two distinct messages | 2 x 48-byte keys + 2 x 96-byte proofs + 2 x 32-byte messages + 96-byte signature | | 1,093,955,874 | | 144,576 | |
| `aggregateVerifyIC` | Verify IC aggregate, two distinct messages | 2 x 96-byte keys + 2 x 32-byte messages + 48-byte signature | | 468,894,403 | | 66,240 | |
| `fastAggregateVerifyEthereum` | Verify Ethereum aggregate, two keys and one message | 2 x 48-byte keys + 2 x 96-byte proofs + 32-byte message + 96-byte signature | | 887,685,951 | | 119,160 | |
| `g1Add` | Add two validated points | 2 x 48-byte points | 328,864,468 | 28,411,996 | 12,622,968 | 104 | 11.57× |
| `g1Aggregate` | Sum four validated points | 4 points (192 bytes) | 654,545,677 | 53,140,794 | 25,008,784 | 104 | 12.32× |
| `g1Msm` | 8-term multi-scalar multiplication | 8 points (384 bytes) + 8 scalars (256 bytes) | 3,105,266,039 | 251,337,606 | 118,021,536 | 1,280 | 12.35× |
| `g1Mul` | Multiply point by scalar | 48-byte point + 32-byte scalar | 393,866,023 | 54,272,206 | 15,095,720 | 104 | 7.26× |
| `g2Add` [U] | Add two validated points | 2 x 96-byte points | 168,180,323 | 75,738,386 | 7,255,832 | 152 | 2.22× |
| `g2Aggregate` [U] | Sum four validated points | 4 points (384 bytes) | 329,778,612 | 148,143,245 | 14,141,336 | 152 | 2.23× |
| `g2Msm` [P] | 8-term multi-scalar multiplication, repeated point | 8 points (768 bytes) + 8 scalars (256 bytes) | 4,617,878,655 | 805,297,631 | 186,921,344 | 2,096 | 5.73× |
| `g2Mul` [P] | Multiply point by scalar | 96-byte point + 32-byte scalar | 572,985,181 | 168,868,760 | 23,188,592 | 152 | 3.39× |
| `hashToG1` | RFC 9380 hash-to-curve | 3-byte message + 50-byte DST | | 15,834,345 | | 104 | |
| `hashToG2` | RFC 9380 hash-to-curve | 3-byte message + 50-byte DST | | 65,993,104 | | 152 | |
| `mapToG1` | Map field element to curve | 48-byte field element | 87,018,850 | 11,894,066 | 3,721,672 | 104 | 7.32× |
| `mapToG2` | Map extension-field element to curve | 96-byte field element (2 x 48 bytes) | 504,926,422 | 49,488,495 | 19,800,200 | 152 | 10.20× |
| `pairingCheck` [P] | Pairing product, two cancelling terms | 2 pairs (288 bytes: 2 x (48 + 96)) | 676,099,164 | 99,939,097 | 31,422,544 | 696 | 6.77× |
| `validateG1` | Validate compressed subgroup point | 48-byte point | 163,459,310 | 12,247,316 | 6,204,312 | 104 | 13.35× |
| `validateG2` [U] | Validate subgroup point | 96-byte point | 82,316,657 | 35,782,534 | 3,492,640 | 152 | 2.30× |
| `verifyEthereum` | Verify Ethereum POP-profile signature | 48-byte key + 32-byte message + 96-byte signature | | 286,386,819 | | 39,280 | |
| `verifyEthereumProof` | Verify proof of possession | 48-byte key + 96-byte proof | | 286,386,744 | | 39,280 | |
| `verifyIC` | Verify IC NUL signature | 96-byte key + 46-byte message + 48-byte signature | | 236,236,649 | | 39,280 | |

[U] Native decodes and subgroup-validates 256-byte EIP-2537 uncompressed points; plugin accepts 96-byte compressed points and also recovers coordinates. Both encode compressed outputs. [P] Native starts from trusted predecoded points/scalars; arithmetic result encoding is outside timing. Plugin includes byte decoding, validation and encoding. Pairing returns a Boolean. These rows compare the same mathematical inputs and outputs with different API work, so their ratios are not equivalent-contract speedups.

## Bitcoin/Hash

| Public function | Workload | Size | Native instructions/op | Plugin instructions/op | Native allocated B/op | Plugin allocated B/op | Native/plugin |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `digest256` | Double SHA256 compatibility alias | 32-byte message | 72,334 | 9,792 | 7,704 | 112 | 7.39× |
| `hash160` | SHA256 followed by RIPEMD160 | 32-byte message | 81,797 | 8,622 | 4,880 | 104 | 9.49× |
| `sha256d` | Double SHA256 | 32-byte message | 72,334 | 9,792 | 7,704 | 112 | 7.39× |
| `taggedHash` | Compute BIP340 tagged SHA256 | 8-byte TapTweak tag + 32-byte message | 120,329 | 35,855 | 8,928 | 1,024 | 3.36× |
| `taggedHasher` | Construct cached-tag hash closure | 8-byte TapTweak tag; 32-byte check message outside timing | 33,410 | 5,956 | 3,752 | 96 | 5.61× |
| `taggedHasher.call` | Invoke cached-tag hash closure | 32-byte message; 8-byte tag prepared outside timing | 64,028 | 30,309 | 3,712 | 968 | 2.11× |

## Bitcoin/Address

| Public function | Workload | Size | Native instructions/op | Plugin instructions/op | Native allocated B/op | Plugin allocated B/op | Native/plugin |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `decode` | Decode mainnet witness-v0 address and check network | 42-character address / 20-byte program | 67,238 | 17,802 | 3,408 | 1,296 | 3.78× |
| `encode` | Encode mainnet witness-v0 address | 20-byte program | 139,514 | 14,624 | 6,712 | 1,064 | 9.54× |
| `hrp` | Select regtest human-readable address prefix | 1 regtest network variant | 324 | 324 | 0 | 0 | 1.00× |
| `p2pkh` | Validate compressed key and derive mainnet P2PKH address | 33-byte compressed SEC1 public key | 24,993,652 | 607,053 | 995,792 | 1,224 | 41.17× |
| `p2sh` | HASH160 redeem script and encode mainnet P2SH address | 34-byte redeem script | 204,638 | 36,021 | 14,232 | 1,128 | 5.68× |
| `p2tr` | Tweak empty-tree internal key and encode mainnet Taproot address | 32-byte internal x-only key | 152,356,558 | 7,021,150 | 6,677,888 | 3,120 | 21.70× |
| `p2trOutput` | Validate and encode tweaked mainnet Taproot output key | 32-byte tweaked x-only key | 25,068,336 | 602,921 | 990,584 | 2,000 | 41.58× |
| `p2wpkh` | Validate compressed key and derive mainnet P2WPKH address | 33-byte compressed SEC1 public key | 25,011,677 | 595,337 | 993,128 | 1,336 | 42.01× |
| `p2wsh` | SHA256 witness script and encode mainnet P2WSH address | 34-byte witness script | 236,505 | 27,390 | 12,680 | 1,568 | 8.63× |
| `scriptPubKey` | Decode and network-check P2PKH address, build locking script | 34-character address / 25-byte output script | 292,855 | 33,655 | 20,504 | 1,536 | 8.70× |
| `signTaprootKeyPath` | Tweak internal secret and BIP340-sign supplied key-path digest | 32-byte secret + 32-byte digest + 32-byte auxiliary randomness | | 25,505,611 | | 1,808 | |
| `taprootOutputKey` | Compute empty-tree Taproot output key and parity | 32-byte internal x-only key | 152,156,951 | 6,999,561 | 6,669,200 | 1,608 | 21.74× |
| `taprootSecretKey` [S] | Derive empty-tree Taproot signing scalar | 32-byte full-width internal secret | 248,262,603 | 12,742,701 | 10,899,648 | 1,656 | 19.48× |

[S] Native composes published curve/scalar arithmetic and tagged hashing for the same BIP341 secret tweak. Its secret arithmetic is variable-time.

## Bitcoin/Bip32

| Public function | Workload | Size | Native instructions/op | Plugin instructions/op | Native allocated B/op | Plugin allocated B/op | Native/plugin |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `deriveChild` | Derive nonhardened child zero from official BIP32 vector-2 root | 78-byte parent payload / 1 child index / depth 1 | 171,779,710 | 7,592,952 | 7,319,408 | 1,776 | 22.62× |
| `derivePath` | Derive two nonhardened children from official BIP32 vector-2 root | 78-byte parent payload / indices [0,1] / depth 2 | 321,348,873 | 14,612,141 | 13,764,984 | 3,488 | 21.99× |
| `deriveTextPath` | Parse textual path and derive two nonhardened children | 78-byte parent payload + m/0/1 (5 bytes) / depth 2 | 321,409,635 | 14,615,878 | 13,771,120 | 3,800 | 21.99× |
| `fingerprint` | Validate compressed key and compute HASH160 fingerprint | 33-byte compressed SEC1 public key / 4-byte result | 24,822,041 | 582,025 | 980,408 | 368 | 42.65× |
| `fromPublicKey` | Validate and construct depth-zero mainnet extended public key | 33-byte compressed key + 32-byte chain code | 24,740,381 | 572,681 | 975,720 | 200 | 43.20× |
| `parse` | Parse and validate official BIP32 vector-2 root xpub | 111-character xpub / 78-byte payload | 25,098,210 | 629,946 | 992,480 | 2,336 | 39.84× |
| `parsePath` | Parse strict nonhardened textual path | m/0/1 (5 UTF-8 bytes) / 2 indices | 59,842 | 2,994 | 5,944 | 152 | 19.99× |
| `serialize` | Serialize official BIP32 vector-2 root xpub | 78-byte node payload / depth zero | 25,120,503 | 639,777 | 988,384 | 2,120 | 39.26× |

## Bitcoin/Script

| Public function | Workload | Size | Native instructions/op | Plugin instructions/op | Native allocated B/op | Plugin allocated B/op | Native/plugin |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `p2pkh` | Build P2PKH locking script | 20-byte public-key hash / 25-byte script | 47,742 | 6,683 | 1,952 | 280 | 7.14× |
| `p2sh` | Build P2SH locking script | 20-byte script hash / 23-byte script | 43,840 | 6,373 | 1,848 | 256 | 6.88× |
| `p2tr` | Build witness-v1 Taproot locking script | 32-byte output key / 34-byte script | 59,396 | 9,471 | 2,392 | 400 | 6.27× |
| `p2wpkh` | Build witness-v0 P2WPKH locking script | 20-byte key hash / 22-byte script | 41,761 | 6,483 | 1,824 | 288 | 6.44× |
| `p2wsh` | Build witness-v0 P2WSH locking script | 32-byte script hash / 34-byte script | 59,351 | 9,481 | 2,392 | 400 | 6.26× |
| `pushData` | Build minimally encoded PUSHDATA1 | 80-byte data / 82-byte script | 115,933 | 21,327 | 3,936 | 832 | 5.44× |
| `tapBranchHash` | Sort child commitments and compute BIP341 TapBranch hash | 2 x 32-byte child hashes | 160,077 | 39,753 | 10,376 | 1,256 | 4.03× |
| `tapLeafHash` | Compute BIP341 current-version TapLeaf commitment | 34-byte tapscript / leaf version 0xc0 | 137,278 | 32,200 | 9,864 | 1,112 | 4.26× |
| `witnessProgram` | Build future witness-v2 locking script | 32-byte program / 34-byte script | 59,673 | 9,429 | 2,424 | 400 | 6.33× |

## Bitcoin/Transaction

| Public function | Workload | Size | Native instructions/op | Plugin instructions/op | Native allocated B/op | Plugin allocated B/op | Native/plugin |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `SegwitV0Hasher.signatureHash` | Compute first cold BIP143 digest from prepared hasher | 10 inputs / 2 outputs / input index 0 / ALL / 25-byte scriptCode | | 245,557 | | 9,056 | |
| `TaprootHasher.signatureHash` [C] | Compute first cold BIP341 digest from prepared hasher | 10 inputs / 2 outputs / input index 0 / DEFAULT / 34-byte spent scripts | 1,323,009 | 400,300 | 60,408 | 14,624 | 3.31× |
| `parse` | Parse complete legacy transaction | 494 wire bytes / 10 inputs / 2 outputs | 261,824 | 131,979 | 14,728 | 6,984 | 1.98× |
| `prepareSegwitV0` | Validate transaction and construct lazy BIP143 hasher | 10 inputs / 2 outputs; check digest outside timing | | 21,213 | | 1,968 | |
| `prepareTaproot` | Validate transaction and prevouts, construct lazy BIP341 hasher | 10 inputs / 2 outputs / 10 spent outputs; check digest outside timing | | 30,389 | | 2,928 | |
| `serialize` | Serialize complete legacy transaction | 10 inputs / 2 outputs / 494 wire bytes | 552,769 | 144,609 | 30,880 | 6,760 | 3.82× |
| `signatureHashLegacy` | Compute one complete legacy SIGHASH\_ALL digest | 10 inputs / 2 outputs / input index 0 / 25-byte scriptCode | 1,076,205 | 200,447 | 50,592 | 7,136 | 5.37× |
| `signatureHashSegwitV0` | Compute one complete BIP143 SIGHASH\_ALL digest | 10 inputs / 2 outputs / input index 0 / 25-byte scriptCode | | 266,530 | | 11,024 | |
| `signatureHashTaproot` | Compute one complete BIP341 key-path DEFAULT digest | 10 inputs / 2 outputs / input index 0 / equal 34-byte spent scripts | 1,323,009 | 430,460 | 60,408 | 17,552 | 3.07× |
| `txid` | Serialize without witness and compute display-order transaction ID | 10 inputs / 2 outputs / 494 wire bytes | 770,490 | 196,503 | 38,904 | 7,248 | 3.92× |
| `wtxid` | Serialize with witness option and compute display-order transaction ID | 10 inputs / 2 outputs / empty witness stacks / 494 wire bytes | 776,045 | 197,444 | 39,568 | 7,248 | 3.93× |

[C] Native computes an uncached one-shot Taproot hash; plugin measures the first cold call on a prepared hasher. Preparation is excluded, lazy common hashes are included. Native returns an array with oracle conversion outside timing; plugin returns a Blob.

Pure Motoko secret-key arithmetic can be variable-time; ratios compare mathematical outputs, not side-channel guarantees. The pinned packages lack BIP340, complete RFC 9380/BLS signature profiles, BIP143, and retained Taproot-hasher construction counterparts; those cells remain empty. Per-row explanations and all adapter sources are in the evidence file.

---

<div class="post-metadata">

**Author:** ![skilesare](https://sea1.discourse-cdn.com/flex023/user_avatar/forum.dfinity.org/skilesare/32/5609_2.png) [@skilesare](https://forum.dfinity.org/u/skilesare)\
**Post date:** [October 12, 2026, 3:55pm UTC](https://forum.dfinity.org/t/motoko-compiler-plugins-for-fast-computation/76041/5 "2026-10-12T15:55:34Z")

</div>

We released [https://mops.one/bls12-381](https://mops.one/bls12-381) last week as part of a motoko-agent! 😃

---

<div class="post-metadata">

**Author:** ![infu](https://sea1.discourse-cdn.com/flex023/user_avatar/forum.dfinity.org/infu/32/11321_2.png) [@infu](https://forum.dfinity.org/u/infu)\
**Post date:** [October 13, 2026, 2:02pm UTC](https://forum.dfinity.org/t/motoko-compiler-plugins-for-fast-computation/76041/6 "2026-10-13T14:02:59Z")

</div>

A bit more human readable and after another round of optimizations. This info will be interesting to Rust-icians as well, the plugins are written in Rust.  
Keep in mind, they are made for the purpose of building the Plugin SDK and checking what the performance benefits will be, not claiming they are bug-free.  
ML-DSA-44 and BLAKE3 are **quantum resistant**.

| Plugin | Input | Sign instructions | Verify instructions | Allocated B, sign / verify |
| --- | --- | --- | --- | --- |
| Ed25519 | 32-byte message | 1,802,738 | 2,306,648 | 120 / 32 |
| Secp256k1 ECDSA | 32-byte SHA-256 digest | 3,618,817 | 2,735,020 | 120 / 32 |
| Secp256k1 BIP340 | 32-byte message | 5,099,055 | 2,519,140 | 120 / 32 |
| P256 ECDSA | 32-byte SHA-256 digest | 7,225,034 | 12,873,707 | 120 / 32 |
| RSA-3072 PSS | 32-byte SHA-256 digest; 32-byte signing salt | 550,857,988 | 9,822,506 | 440 / 32 |
| ML-DSA-44 | 1-byte message; 59-byte context | 14,753,165 | 3,806,359 | 2,480 / 32 |
| BLAKE3 (shared-secret) | 1,024-byte message; 32-byte secret key | 35,935 | 40,819 | 88 / 88 |

## IC certified-data relay verification

An untrusted client can carry A’s certificate and Merkle witness to B. Caching  
A’s authenticated certified root lets every client proving a record against  
that root skip repeated BLS verification.

| Verification path | Instructions, 1,000 entries | Instructions, 100,000 entries | Allocated B, 1,000 / 100,000 entries |
| --- | --- | --- | --- |
| Cold: root delegation and subnet signatures | 423,376,817 | 423,716,169 | 102,320 / 117,552 |
| Cached verified delegation and prepared subnet key | 168,737,010 | 169,076,362 | 65,408 / 80,640 |
| Cached authenticated A root; application proof only | **565,546** | **904,898** | **26,176 / 41,408** |

---

<div class="post-metadata">

**Author:** ![skilesare](https://sea1.discourse-cdn.com/flex023/user_avatar/forum.dfinity.org/skilesare/32/5609_2.png) [@skilesare](https://forum.dfinity.org/u/skilesare)\
**Post date:** [October 26, 2026, 2:47pm UTC](https://forum.dfinity.org/t/motoko-compiler-plugins-for-fast-computation/76041/7 "2026-10-26T14:47:33Z")

</div>

**Fast Rust inside Motoko canisters, without giving up Motoko: native kernels in moxzi alpha.10**

Thanks to @infu for this thread, for [PR #6443](https://github.com/caffeinelabs/motoko/pull/6443) and for the plugins themselves (now at [Neutrinomic/motoko\_plugins](https://github.com/Neutrinomic/motoko_plugins)): the idea of calling native code from Motoko and every baseline measurement below come from @infu. It made us measure something we had been hand-waving about, and then build it. Here is what came out, written so it makes sense even if you have never compiled a canister by hand.

**The problem, in a few numbers.** Every message a canister handles has an instruction budget (40 billion on an ordinary subnet), and every instruction costs cycles. Cryptography is where Motoko hurts: verifying an Internet Computer certificate (one or two BLS signature checks) costs about 2 billion instructions in Motoko, SHA-256 of 1 MB about 405 million, a secp256k1 signature about 66 million. All perfectly writable in Motoko, and all several times to dozens of times more expensive than the same work done by a mature Rust crate, because crypto lives on low-level tricks (64-bit limb arithmetic, precomputed tables, hand-tuned field multiplication) that Motoko doesn’t give you.

**The idea from this thread.** Let a Motoko program call fast code written in another language. The plugins @infu showed did that by building the Rust into the compiler. We took the same idea and made it a library feature instead:

- A **kernel** is a tiny WebAssembly library built from an existing, well-known Rust crate (RustCrypto, dalek, zkcrypto, libcrux…). It ships inside an ordinary mops package.
- The package’s Motoko function **keeps its Motoko body**. Under stock `moc`, or if you don’t opt in, that body runs and your program behaves exactly as before. With moxzi and the opt-in, the compiler calls the kernel instead and links it only into canisters that actually use it.
- You opt in from **your own** `mops.toml`, pinning the exact bytes:

`toml [kernels] moxzi-hash = "sha256:d9e07eab…" `

A dependency can never add native code behind your back, and if the bytes on disk don’t match your pin, the build stops instead of falling back silently.

Same source, two compilers, same answers: the Motoko body doubles as the test oracle, and every kernel is checked against it (and against official test vectors) on every build of moxzi.

**What it buys.** Instructions per call, compared with the plugins @infu benchmarked earlier in this thread (the “Plugin” column is @infu’s published numbers; we measured our kernels the same way; lower is better):

| Operation | Plugin (this thread) | moxzi kernel |
| --- | --- | --- |
| secp256k1 ECDSA sign | 3.6 M | 1.5 M |
| secp256k1 BIP340 Schnorr sign | 5.1 M | 2.1 M |
| P-256 verify | 12.9 M | 8.4 M |
| Ed25519 verify | 2.3 M | 2.2 M |
| RSA-3072 verify | 9.8 M | 5.0 M |
| ML-DSA-44 sign (post-quantum) | 14.8 M | 9.0 M |
| BLS12-381 pairing check | 100 M | 64 M |

And the one that started it: `mo:ic-agent` verifies a mainnet certificate in **about 0.37 billion instructions** with its kernel, against about 2 billion with the Motoko body alone (0.61 vs 3.8 billion with a subnet delegation). SHA-256 of 1 MB drops from 405 million to 76 million. Hashing (SHA-256/512, HMAC, RIPEMD-160, BLAKE3), Base58, Bech32 and Bitcoin script/transaction helpers are there too. Ten kernel packages in all, plus the BLS kernel inside `mo:ic-agent`.

A side quest from this thread: we also made moxzi’s _built-in_ `to_candid` encoder faster. On @infu’s CandidX benchmark (1,024 records; the plugin does it in about 352 K instructions), the built-in encoder went from 832 K to 165 K instructions, with output byte-identical to moc’s — no plugin needed.

**“Native code in my canister” should make you nervous. Here’s what we did about it.**

A kernel shares your canister’s memory: a buggy or malicious one could read or write anything in it. So:

1. **You choose, by hash.** Kernels we build and publish are recognised by their exact bytes; any other kernel stops the build the first time and shows you its source and what it can do until you explicitly accept that exact version.
2. **Anyone can rebuild them.** Each kernel is built from pinned, locked sources with a pinned toolchain, and rebuilds to identical bytes on macOS and Linux (one Docker command checks all of them). The canister’s public metadata lists every kernel it contains, with its hash and source, so a verifier can tell what’s inside a deployed module.
3. **We attacked our own code.** Constant-time code is the classic crypto pitfall: if signing takes longer for some keys than others, the key leaks. On the IC the observable is the instruction count, so we test that every secret-key operation costs exactly the same for every key. That test found four real leaks in our own code, now fixed. Then we went further and scanned the _compiled_ WebAssembly for branches on secret data — and found that the compiler had quietly turned constant-time Rust into branches in two places, invisible to the instruction count but real on machines that run the same code natively. Fixed, and the scan now runs on every build.
4. **Adversarial review.** Independent reviewers went through the RSA, secp256k1, P-256, Ed25519 and BLS code looking for wrong results and leaks, with a failing test required for every claim.

What we have _not_ done yet is an independent expert audit of the hand-written parts (RSA’s arithmetic, the curve tables). Until that happens, please treat those kernels as fast and well-tested, but not audited: fine for experiments, prototypes and verification work, not yet for guarding real value.

**Try it.** Install moxzi alpha.10, add one of the `moxzi-*` packages, add the `[kernels]` pin, and build. The docs page “Native kernels” walks through it with a SHA-256 example that shows the instruction count with and without the kernel. Writing your own kernel from a Rust crate is `moxzi kernel build`.

**If moc ever adopts compute plugins:** our design deliberately keeps programs moc-compatible (the Motoko body is always there) and names kernels by package and function, so the two approaches could meet. We’d be glad to line the names and the calling convention up with whatever the Motoko team prefers.

The kernels live in their own public repository, [icdevsorg/moxzi-kernels](https://github.com/icdevsorg/moxzi-kernels) (MIT OR Apache-2.0), maintained by [ICDevs.org](http://ICDevs.org) (a 501c3 non-profit); if they’re useful to you, please consider supporting that work at [https://icdevs.org/donation.html](https://icdevs.org/donation.html). moxzi itself is built by RIVVIR Tech.

Questions, kernel requests and “you’re wrong about X” all welcome.

Thanks to @infu for this thread, for [PR #6443]([https://github.com/caffeinelabs/motoko/pull/6443](https://github.com/caffeinelabs/motoko/pull/6443)) and for the plugins themselves (now at [Neutrinomic/motoko\_plugins]([https://github.com/Neutrinomic/motoko\_plugins](https://github.com/Neutrinomic/motoko_plugins))): the idea of calling native code from Motoko and every baseline measurement below come from @infu. It made us measure something we had been hand-waving about, and then build it. Here is what came out, written so it makes sense even if you have never compiled a canister by hand.

**\*\*The problem, in a few numbers.\*\*** Every message a canister handles has an instruction budget (40 billion on an ordinary subnet), and every instruction costs cycles. Cryptography is where Motoko hurts: verifying an Internet Computer certificate (one or two BLS signature checks) costs about 2 billion instructions in Motoko, SHA-256 of 1 MB about 405 million, a secp256k1 signature about 66 million. All perfectly writable in Motoko, and all several times to dozens of times more expensive than the same work done by a mature Rust crate, because crypto lives on low-level tricks (64-bit limb arithmetic, precomputed tables, hand-tuned field multiplication) that Motoko doesn’t give you.

**\*\*The idea from this thread.\*\*** Let a Motoko program call fast code written in another language. The plugins @infu showed did that by building the Rust into the compiler. We took the same idea and made it a library feature instead:

- A **\*\*kernel\*\*** is a tiny WebAssembly library built from an existing, well-known Rust crate (RustCrypto, dalek, zkcrypto, libcrux…). It ships inside an ordinary mops package.

- The package’s Motoko function **\*\*keeps its Motoko body\*\***. Under stock `moc`, or if you don’t opt in, that body runs and your program behaves exactly as before. With moxzi and the opt-in, the compiler calls the kernel instead and links it only into canisters that actually use it.

- You opt in from **\*\*your own\*\*** `mops.toml`, pinning the exact bytes:

```toml [kernels] moxzi-hash = “sha256:d9e07eab…” ```

A dependency can never add native code behind your back, and if the bytes on disk don’t match your pin, the build stops instead of falling back silently.

Same source, two compilers, same answers: the Motoko body doubles as the test oracle, and every kernel is checked against it (and against official test vectors) on every build of moxzi.

**\*\*What it buys.\*\*** Instructions per call, compared with the plugins @infu benchmarked earlier in this thread (the “Plugin” column is @infu’s published numbers; we measured our kernels the same way; lower is better):

| Operation | Plugin (this thread) | moxzi kernel |

|—|—:|—😑

| secp256k1 ECDSA sign | 3.6 M | 1.5 M |

| secp256k1 BIP340 Schnorr sign | 5.1 M | 2.1 M |

| P-256 verify | 12.9 M | 8.4 M |

| Ed25519 verify | 2.3 M | 2.2 M |

| RSA-3072 verify | 9.8 M | 5.0 M |

| ML-DSA-44 sign (post-quantum) | 14.8 M | 9.0 M |

| BLS12-381 pairing check | 100 M | 64 M |

And the one that started it: `mo:ic-agent` verifies a mainnet certificate in **\*\*about 0.37 billion instructions\*\*** with its kernel, against about 2 billion with the Motoko body alone (0.61 vs 3.8 billion with a subnet delegation). SHA-256 of 1 MB drops from 405 million to 76 million. Hashing (SHA-256/512, HMAC, RIPEMD-160, BLAKE3), Base58, Bech32 and Bitcoin script/transaction helpers are there too. Ten kernel packages in all, plus the BLS kernel inside `mo:ic-agent`.

A side quest from this thread: we also made moxzi’s _\*built-in\*_ `to_candid` encoder faster. On @infu’s CandidX benchmark (1,024 records; the plugin does it in about 352 K instructions), the built-in encoder went from 832 K to 165 K instructions, with output byte-identical to moc’s — no plugin needed.

**\*\*“Native code in my canister” should make you nervous. Here’s what we did about it.\*\***

A kernel shares your canister’s memory: a buggy or malicious one could read or write anything in it. So:

1. **\*\*You choose, by hash.\*\*** Kernels we build and publish are recognised by their exact bytes; any other kernel stops the build the first time and shows you its source and what it can do until you explicitly accept that exact version.

2. **\*\*Anyone can rebuild them.\*\*** Each kernel is built from pinned, locked sources with a pinned toolchain, and rebuilds to identical bytes on macOS and Linux (one Docker command checks all of them). The canister’s public metadata lists every kernel it contains, with its hash and source, so a verifier can tell what’s inside a deployed module.

3. **\*\*We attacked our own code.\*\*** Constant-time code is the classic crypto pitfall: if signing takes longer for some keys than others, the key leaks. On the IC the observable is the instruction count, so we test that every secret-key operation costs exactly the same for every key. That test found four real leaks in our own code, now fixed. Then we went further and scanned the _\*compiled\*_ WebAssembly for branches on secret data — and found that the compiler had quietly turned constant-time Rust into branches in two places, invisible to the instruction count but real on machines that run the same code natively. Fixed, and the scan now runs on every build.

4. **\*\*Adversarial review.\*\*** Independent reviewers went through the RSA, secp256k1, P-256, Ed25519 and BLS code looking for wrong results and leaks, with a failing test required for every claim.

What we have _\*not\*_ done yet is an independent expert audit of the hand-written parts (RSA’s arithmetic, the curve tables). Until that happens, please treat those kernels as fast and well-tested, but not audited: fine for experiments, prototypes and verification work, not yet for guarding real value.

**\*\*Try it.\*\*** Install moxzi alpha.10, add one of the `moxzi-*` packages, add the `[kernels]` pin, and build. The docs page “Native kernels” walks through it with a SHA-256 example that shows the instruction count with and without the kernel. Writing your own kernel from a Rust crate is `moxzi kernel build`.

**\*\*If moc ever adopts compute plugins:\*\*** our design deliberately keeps programs moc-compatible (the Motoko body is always there) and names kernels by package and function, so the two approaches could meet. We’d be glad to line the names and the calling convention up with whatever the Motoko team prefers.

The kernels live in their own public repository, [icdevsorg/moxzi-kernels]([https://github.com/icdevsorg/moxzi-kernels](https://github.com/icdevsorg/moxzi-kernels)) (MIT OR Apache-2.0), maintained by [ICDevs.org](http://ICDevs.org) (a 501c3 non-profit); if they’re useful to you, please consider supporting that work at [https://icdevs.org/donation.html](https://icdevs.org/donation.html). moxzi itself is built by RIVVIR Tech.

Questions, kernel requests and “you’re wrong about X” all welcome.

More here: [https://forum.dfinity.org/t/moxzi-0-1-0-alpha-10-fast-rust-inside-motoko-faster-candid-and-builds-you-can-check/76238](https://forum.dfinity.org/t/moxzi-0-1-0-alpha-10-fast-rust-inside-motoko-faster-candid-and-builds-you-can-check/76238)
