From 3137d3b099ec2ec146f8367708767b12d370a1a2 Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Tue, 14 Apr 2026 20:15:56 +0200 Subject: [PATCH 01/34] AI run to follow Motoko style guidelines * sorted imports * dot-notation when available * consistent return statements --- bench/ecdsa_verify.bench.mo | 2 +- src/Base58.mo | 6 +++--- src/Base58Check.mo | 4 ++-- src/Bech32.mo | 12 +++++------ src/Bip32.mo | 22 +++++++++---------- src/ByteUtils.mo | 27 +++++++++++------------ src/Common.mo | 6 +++--- src/Hash.mo | 2 +- src/Hmac.mo | 2 +- src/Ripemd160.mo | 13 ++++++----- src/Segwit.mo | 4 ++-- src/bitcoin/Address.mo | 8 +++---- src/bitcoin/Bitcoin.mo | 20 ++++++++--------- src/bitcoin/P2pkh.mo | 8 +++---- src/bitcoin/P2tr.mo | 10 ++++----- src/bitcoin/Script.mo | 4 ++-- src/bitcoin/Transaction.mo | 36 +++++++++++++++---------------- src/bitcoin/TxInput.mo | 7 +++--- src/bitcoin/TxOutput.mo | 7 +++--- src/bitcoin/Wif.mo | 2 +- src/bitcoin/Witness.mo | 6 +++--- src/ec/Affine.mo | 9 ++++---- src/ec/Curves.mo | 2 +- src/ec/Field.mo | 6 +++--- src/ec/Fp.mo | 2 +- src/ec/Jacobi.mo | 10 ++++----- src/ec/Numbers.mo | 6 +++--- src/ecdsa/Der.mo | 13 ++++++----- src/ecdsa/Ecdsa.mo | 2 +- src/ecdsa/Publickey.mo | 4 ++-- test/Hex.mo | 3 +-- test/bitcoin/bitcoin.test.mo | 2 +- test/bitcoin/bitcoinTestTools.mo | 3 +-- test/bitcoin/p2pkhSighash.test.mo | 2 +- test/common.test.mo | 8 +++---- test/ec/jacobi.test.mo | 6 +++--- test/ecdsa/ecdsa.test.mo | 2 +- test/segwit.test.mo | 3 +-- 38 files changed, 141 insertions(+), 150 deletions(-) diff --git a/bench/ecdsa_verify.bench.mo b/bench/ecdsa_verify.bench.mo index dc8e505..0c19e8c 100644 --- a/bench/ecdsa_verify.bench.mo +++ b/bench/ecdsa_verify.bench.mo @@ -40,7 +40,7 @@ module { oi += 1; i += 2; }; - Array.fromVarArray(out); + out.toArray(); }; public func init() : Bench.V1 { diff --git a/src/Base58.mo b/src/Base58.mo index 6a199b3..d75386c 100755 --- a/src/Base58.mo +++ b/src/Base58.mo @@ -1,9 +1,9 @@ import Array "mo:core/Array"; import Char "mo:core/Char"; -import { type Iter } "mo:core/Types"; import Nat32 "mo:core/Nat32"; import Nat8 "mo:core/Nat8"; import Text "mo:core/Text"; +import { type Iter } "mo:core/Types"; import VarArray "mo:core/VarArray"; module { @@ -143,7 +143,7 @@ module { }, ); - return output; + output; }; // Convert the given Base256 input to Base58. @@ -197,6 +197,6 @@ module { }; }, ); - return Text.fromIter(output.values()); + Text.fromIter(output.values()); }; }; diff --git a/src/Base58Check.mo b/src/Base58Check.mo index 0717882..81e389c 100755 --- a/src/Base58Check.mo +++ b/src/Base58Check.mo @@ -5,7 +5,7 @@ import VarArray "mo:core/VarArray"; import Sha256 "mo:sha2/Sha256"; -import Base58 "./Base58"; +import Base58 "Base58"; module { @@ -24,7 +24,7 @@ module { inputWithCheck[input.size() + 2] := hash[2]; inputWithCheck[input.size() + 3] := hash[3]; - return Base58.encode(Array.fromVarArray(inputWithCheck)); + Base58.encode(inputWithCheck.toArray()); }; // Convert the given checked Base58 input to Base256. Returns null if the diff --git a/src/Bech32.mo b/src/Bech32.mo index 2c9a87d..b1b48ff 100644 --- a/src/Bech32.mo +++ b/src/Bech32.mo @@ -1,11 +1,11 @@ import Array "mo:core/Array"; import Blob "mo:core/Blob"; import Char "mo:core/Char"; -import { type Result } "mo:core/Types"; import Nat "mo:core/Nat"; import Nat32 "mo:core/Nat32"; import Nat8 "mo:core/Nat8"; import Text "mo:core/Text"; +import { type Result } "mo:core/Types"; import VarArray "mo:core/VarArray"; module { @@ -71,7 +71,7 @@ module { assert output.size() <= 90; - return Text.fromArray(output); + Text.fromArray(output); }; // Decode given text as Bech32 or Bech32m. @@ -162,7 +162,7 @@ module { output[i + hrpSize + 1] := currHrp & 0x1f; }; - return Array.fromVarArray(output); + output.toArray(); }; // Constant value associated to the given encoding. @@ -192,7 +192,7 @@ module { let mod : Nat32 = polymod(polyModValues) ^ encodingConstant(encoding); // Convert the 5-bit groups in mod to checksum data. - return Array.tabulate( + Array.tabulate( 6, func(i) { Nat8.fromIntWrap( @@ -209,7 +209,7 @@ module { let check : Nat32 = polymod(expandedHrp.concat(values)); - return if (check == encodingConstant(#BECH32)) { + if (check == encodingConstant(#BECH32)) { #ok(#BECH32); } else if (check == encodingConstant(#BECH32M)) { #ok(#BECH32M); @@ -235,7 +235,7 @@ module { if (c0 & 8 > 0) c ^= 0x3d4233dd; if (c0 & 16 > 0) c ^= 0x2a1462b3; }; - return c; + c; }; // If input corresponds to code of uppercase character, return code of its diff --git a/src/Bip32.mo b/src/Bip32.mo index 716e669..6b5360c 100644 --- a/src/Bip32.mo +++ b/src/Bip32.mo @@ -1,19 +1,19 @@ import Array "mo:core/Array"; import Blob "mo:core/Blob"; -import { type Iter } "mo:core/Types"; import Iter "mo:core/Iter"; import Nat "mo:core/Nat"; import Nat32 "mo:core/Nat32"; import Text "mo:core/Text"; +import { type Iter } "mo:core/Types"; import VarArray "mo:core/VarArray"; -import Affine "./ec/Affine"; -import Base58Check "./Base58Check"; -import Common "./Common"; -import Curves "./ec/Curves"; -import Hash "./Hash"; -import Hmac "./Hmac"; -import Jacobi "./ec/Jacobi"; +import Affine "ec/Affine"; +import Base58Check "Base58Check"; +import Common "Common"; +import Curves "ec/Curves"; +import Hash "Hash"; +import Hmac "Hmac"; +import Jacobi "ec/Jacobi"; module { public type Path = { @@ -151,7 +151,7 @@ module { "", ); - let trimmed : Text = switch (Text.stripStart(sanitized, #text "m/")) { + let trimmed : Text = switch (sanitized.stripStart(#text "m/")) { case (?t) t; case (null) sanitized; }; @@ -240,7 +240,7 @@ module { Common.copy(hmacData, 0, key, 0, 33); Common.writeBE32(hmacData, 33, index); let hmacSha512 : Hmac.Hmac = Hmac.sha512(chaincode); - hmacSha512.writeArray(Array.fromVarArray(hmacData)); + hmacSha512.writeArray(hmacData.toArray()); let fullNode : [Nat8] = hmacSha512.sum().toArray(); // Split HMAC output into two 32-byte sequences. @@ -316,7 +316,7 @@ module { Common.copy(result, 13, chaincode, 0, 32); Common.copy(result, 45, key, 0, key.size()); - return Base58Check.encode(Array.fromVarArray(result)); + Base58Check.encode(result.toArray()); }; }; }; diff --git a/src/ByteUtils.mo b/src/ByteUtils.mo index f2728e0..67898f4 100644 --- a/src/ByteUtils.mo +++ b/src/ByteUtils.mo @@ -1,12 +1,11 @@ -import Array "mo:core/Array"; -import { type Iter } "mo:core/Types"; import Nat16 "mo:core/Nat16"; import Nat32 "mo:core/Nat32"; import Nat64 "mo:core/Nat64"; import Nat8 "mo:core/Nat8"; +import { type Iter } "mo:core/Types"; import VarArray "mo:core/VarArray"; -import Common "./Common"; +import Common "Common"; module { // Read a number of elements from the given iterator and return as array. If @@ -38,7 +37,7 @@ module { }; }; - Array.fromVarArray(readData); + readData.toArray(); }; }; @@ -110,18 +109,18 @@ module { public func writeVarint(value : Nat) : [Nat8] { assert (value < 0x10000000000000000); - return if (value < 0xfd) { [Nat8.fromIntWrap(value)] } else if (value < 0x10000) { - let result = VarArray.repeat(0xfd, 3); - Common.writeLE16(result, 1, Nat16.fromIntWrap(value)); - Array.fromVarArray(result); + if (value < 0xfd) { [Nat8.fromIntWrap(value)] } else if (value < 0x10000) { + let buf = VarArray.repeat(0xfd, 3); + Common.writeLE16(buf, 1, Nat16.fromIntWrap(value)); + buf.toArray(); } else if (value < 0x100000000) { - let result = VarArray.repeat(0xfe, 5); - Common.writeLE32(result, 1, Nat32.fromIntWrap(value)); - Array.fromVarArray(result); + let buf = VarArray.repeat(0xfe, 5); + Common.writeLE32(buf, 1, Nat32.fromIntWrap(value)); + buf.toArray(); } else { - let result = VarArray.repeat(0xff, 9); - Common.writeLE64(result, 1, Nat64.fromIntWrap(value)); - Array.fromVarArray(result); + let buf = VarArray.repeat(0xff, 9); + Common.writeLE64(buf, 1, Nat64.fromIntWrap(value)); + buf.toArray(); }; }; }; diff --git a/src/Common.mo b/src/Common.mo index a5cf7d9..ef28ca5 100644 --- a/src/Common.mo +++ b/src/Common.mo @@ -15,7 +15,7 @@ module { let first : Nat32 = readBE32(bytes, offset); let second : Nat32 = readBE32(bytes, offset + 4); - return Nat64.fromIntWrap(first.toNat()) << 32 | Nat64.fromIntWrap(second.toNat()); + Nat64.fromIntWrap(first.toNat()) << 32 | Nat64.fromIntWrap(second.toNat()); }; // Read big endian 128-bit natural number starting at offset. @@ -23,7 +23,7 @@ module { let first : Nat64 = readBE64(bytes, offset); let second : Nat64 = readBE64(bytes, offset + 8); - return first.toNat() * 0x10000000000000000 + second.toNat(); + first.toNat() * 0x10000000000000000 + second.toNat(); }; // Read big endian 256-bit natural number starting at offset. @@ -31,7 +31,7 @@ module { let first : Nat = readBE128(bytes, offset); let second : Nat = readBE128(bytes, offset + 16); - return first * 0x100000000000000000000000000000000 + second; + first * 0x100000000000000000000000000000000 + second; }; // Write given value as 32-bit big endian into array starting at offset. diff --git a/src/Hash.mo b/src/Hash.mo index c2141da..8a16993 100644 --- a/src/Hash.mo +++ b/src/Hash.mo @@ -4,7 +4,7 @@ import Text "mo:core/Text"; import Sha256 "mo:sha2/Sha256"; -import Ripemd160 "./Ripemd160"; +import Ripemd160 "Ripemd160"; module { // Applies SHA256 followed by RIPEMD160 on the given data. diff --git a/src/Hmac.mo b/src/Hmac.mo index 23e5e45..2f2f1b8 100644 --- a/src/Hmac.mo +++ b/src/Hmac.mo @@ -105,7 +105,7 @@ module { public func sum() : Blob { let innerHash = innerDigest.sum().toArray(); outerDigest.writeArray(innerHash); - return outerDigest.sum(); + outerDigest.sum(); }; }; }; diff --git a/src/Ripemd160.mo b/src/Ripemd160.mo index 08b080d..ef14479 100644 --- a/src/Ripemd160.mo +++ b/src/Ripemd160.mo @@ -1,16 +1,15 @@ -import Array "mo:core/Array"; import Nat "mo:core/Nat"; import Nat64 "mo:core/Nat64"; import VarArray "mo:core/VarArray"; -import Common "./Common"; +import Common "Common"; module { // Hash the given array and return finalized result. public func hash(array : [Nat8]) : [Nat8] { let digest = Digest(); digest.write(array); - return digest.sum(); + digest.sum(); }; public class Digest() { @@ -726,7 +725,7 @@ module { }; // Add the count of processed bytes. bytes += 64 - Nat64.fromNat(bufsize); - transform(Array.fromVarArray(buf), 0); + transform(buf.toArray(), 0); // All data in the buffer has been processed, reset buffer data size // point transformOffset to index of not-yet processed data. transformOffset += 64 - bufsize; @@ -758,8 +757,8 @@ module { let sizedesc : [var Nat8] = VarArray.repeat(0, 8); Common.writeLE64(sizedesc, 0, bytes << 3); - write(Array.fromVarArray(pad)); - write(Array.fromVarArray(sizedesc)); + write(pad.toArray()); + write(sizedesc.toArray()); let hash : [var Nat8] = VarArray.repeat(0, 20); Common.writeLE32(hash, 0, s[0]); @@ -768,7 +767,7 @@ module { Common.writeLE32(hash, 12, s[3]); Common.writeLE32(hash, 16, s[4]); - return Array.fromVarArray(hash); + hash.toArray(); }; }; }; diff --git a/src/Segwit.mo b/src/Segwit.mo index e3345cd..7eb85bc 100644 --- a/src/Segwit.mo +++ b/src/Segwit.mo @@ -1,11 +1,11 @@ import List "mo:core/List"; -import { type Result; type Iter } "mo:core/Types"; import Nat "mo:core/Nat"; import Nat32 "mo:core/Nat32"; import Nat8 "mo:core/Nat8"; import Runtime "mo:core/Runtime"; +import { type Result; type Iter } "mo:core/Types"; -import Bech32 "../src/Bech32"; +import Bech32 "Bech32"; module { diff --git a/src/bitcoin/Address.mo b/src/bitcoin/Address.mo index 767629d..16f6d94 100644 --- a/src/bitcoin/Address.mo +++ b/src/bitcoin/Address.mo @@ -1,10 +1,10 @@ import { type Result } "mo:core/Types"; -import P2pkh "./P2pkh"; -import P2tr "./P2tr"; -import Script "./Script"; import Segwit "../Segwit"; -import Types "./Types"; +import P2pkh "P2pkh"; +import P2tr "P2tr"; +import Script "Script"; +import Types "Types"; module { public func addressFromText(address : Text) : Result { diff --git a/src/bitcoin/Bitcoin.mo b/src/bitcoin/Bitcoin.mo index df6c6d9..a92fbfa 100644 --- a/src/bitcoin/Bitcoin.mo +++ b/src/bitcoin/Bitcoin.mo @@ -1,20 +1,20 @@ import Array "mo:core/Array"; import Blob "mo:core/Blob"; import List "mo:core/List"; -import { type Result } "mo:core/Types"; import Nat "mo:core/Nat"; import Nat32 "mo:core/Nat32"; import Nat8 "mo:core/Nat8"; +import { type Result } "mo:core/Types"; import VarArray "mo:core/VarArray"; -import Address "./Address"; import Der "../ecdsa/Der"; -import Script "./Script"; -import Transaction "./Transaction"; -import TxInput "./TxInput"; -import TxOutput "./TxOutput"; -import Types "./Types"; -import Witness "./Witness"; +import Address "Address"; +import Script "Script"; +import Transaction "Transaction"; +import TxInput "TxInput"; +import TxOutput "TxOutput"; +import Types "Types"; +import Witness "Witness"; module { type Utxo = Types.Utxo; @@ -127,7 +127,7 @@ module { // Obtain the scriptPubKey of the source address which is also the // scriptPubKey of the Tx output being spent. - return switch (Address.scriptPubKey(sourceAddress)) { + switch (Address.scriptPubKey(sourceAddress)) { case (#ok scriptPubKey) { // Obtain scriptSigs for each Tx input. let scriptSigs = Array.tabulate( @@ -198,7 +198,7 @@ module { fees : Satoshi, ) : Result { - return switch ( + switch ( buildTransaction( version, utxos, diff --git a/src/bitcoin/P2pkh.mo b/src/bitcoin/P2pkh.mo index 3fac023..eac4aaf 100644 --- a/src/bitcoin/P2pkh.mo +++ b/src/bitcoin/P2pkh.mo @@ -3,11 +3,11 @@ import { type Result; type Iter } "mo:core/Types"; import Base58Check "../Base58Check"; import ByteUtils "../ByteUtils"; +import Hash "../Hash"; import Ecdsa "../ecdsa/Ecdsa"; import EcdsaTypes "../ecdsa/Types"; -import Hash "../Hash"; -import Script "./Script"; -import Types "./Types"; +import Script "Script"; +import Types "Types"; module { type PublicKey = Ecdsa.PublicKey; @@ -67,7 +67,7 @@ module { }; }, ); - return Base58Check.encode(versionedHash); + Base58Check.encode(versionedHash); }; // Decode P2PKH hash into its network and public key hash components. diff --git a/src/bitcoin/P2tr.mo b/src/bitcoin/P2tr.mo index 0f0d4ea..a869976 100644 --- a/src/bitcoin/P2tr.mo +++ b/src/bitcoin/P2tr.mo @@ -1,15 +1,15 @@ import Array "mo:core/Array"; -import { type Result } "mo:core/Types"; import Nat "mo:core/Nat"; +import { type Result } "mo:core/Types"; import Common "../Common"; +import Hash "../Hash"; +import Segwit "../Segwit"; import Curves "../ec/Curves"; import Fp "../ec/Fp"; -import Hash "../Hash"; import Jacobi "../ec/Jacobi"; -import Script "./Script"; -import Segwit "../Segwit"; -import Types "./Types"; +import Script "Script"; +import Types "Types"; module { type PublicKey = { diff --git a/src/bitcoin/Script.mo b/src/bitcoin/Script.mo index 9cf3a4d..e92a5ea 100644 --- a/src/bitcoin/Script.mo +++ b/src/bitcoin/Script.mo @@ -1,10 +1,10 @@ import Array "mo:core/Array"; import List "mo:core/List"; -import { type Iter; type Result } "mo:core/Types"; import Nat16 "mo:core/Nat16"; import Nat32 "mo:core/Nat32"; import Nat8 "mo:core/Nat8"; import Runtime "mo:core/Runtime"; +import { type Iter; type Result } "mo:core/Types"; import VarArray "mo:core/VarArray"; import ByteUtils "../ByteUtils"; @@ -623,7 +623,7 @@ module Script { // Prepend buffer size as varint and return. let encodedBufSize = ByteUtils.writeVarint(buf.size()); - return Array.tabulate( + Array.tabulate( encodedBufSize.size() + buf.size(), func(i) { if (i < encodedBufSize.size()) { diff --git a/src/bitcoin/Transaction.mo b/src/bitcoin/Transaction.mo index 40a0653..ae9cbad 100644 --- a/src/bitcoin/Transaction.mo +++ b/src/bitcoin/Transaction.mo @@ -1,10 +1,10 @@ import Array "mo:core/Array"; import Blob "mo:core/Blob"; import List "mo:core/List"; -import { type Iter; type Result } "mo:core/Types"; import Nat "mo:core/Nat"; import Nat32 "mo:core/Nat32"; import Text "mo:core/Text"; +import { type Iter; type Result } "mo:core/Types"; import VarArray "mo:core/VarArray"; import Sha256 "mo:sha2/Sha256"; @@ -12,10 +12,10 @@ import Sha256 "mo:sha2/Sha256"; import ByteUtils "../ByteUtils"; import Common "../Common"; import Hash "../Hash"; -import Script "./Script"; -import TxInput "./TxInput"; -import TxOutput "./TxOutput"; -import Types "./Types"; +import Script "Script"; +import TxInput "TxInput"; +import TxOutput "TxOutput"; +import Types "Types"; import Witness "Witness"; module { @@ -165,7 +165,7 @@ module { /// the id that includes witness is denoted as wtxid. public func txid() : [Nat8] { let doubleHash : [Nat8] = Hash.doubleSHA256(toBytesIgnoringWitness()); - return Array.tabulate( + Array.tabulate( doubleHash.size(), func(n : Nat) { doubleHash[doubleHash.size() - 1 - n]; @@ -205,7 +205,7 @@ module { Common.copy(output, 0, txData, 0, txData.size()); Common.writeLE32(output, txData.size(), sigHashType); - return Hash.doubleSHA256(Array.fromVarArray(output)); + Hash.doubleSHA256(output.toArray()); }; /// Create a P2TR key spend signature hash for this transaction. This is @@ -254,7 +254,7 @@ module { Common.writeLE32(vout_buffer, 0, txin.prevOutput.vout); let prevout = [ txin.prevOutput.txid.toArray(), - Array.fromVarArray(vout_buffer), + vout_buffer.toArray(), ].flatten(); prevout; } @@ -266,22 +266,22 @@ module { let sighash_type : [Nat8] = [0x00]; let nVersion_buffer = VarArray.repeat(0, 4); Common.writeLE32(nVersion_buffer, 0, 2); - let nVersion = Array.fromVarArray(nVersion_buffer); + let nVersion = nVersion_buffer.toArray(); - let nLockTime : [Nat8] = Array.fromVarArray(VarArray.repeat(0, 4)); + let nLockTime : [Nat8] = VarArray.repeat(0, 4).toArray(); let sha_prevouts : [Nat8] = Sha256.fromArray(#sha256, prevouts.flatten()).toArray(); let amounts_bytes = amounts.map( func(amount) { let amount_bytes = VarArray.repeat(0, 8); Common.writeLE64(amount_bytes, 0, amount); - Array.fromVarArray(amount_bytes); + amount_bytes.toArray(); } ).flatten(); let sha_amounts : [Nat8] = Sha256.fromArray(#sha256, amounts_bytes).toArray(); let scriptpubkeys = VarArray.repeat<[Nat8]>(Script.toBytes(scriptPubKey), txInputs.size()); - let sha_scriptpubkeys : [Nat8] = Sha256.fromArray(#sha256, Array.fromVarArray(scriptpubkeys).flatten()).toArray(); + let sha_scriptpubkeys : [Nat8] = Sha256.fromArray(#sha256, scriptpubkeys.toArray().flatten()).toArray(); // ignote the nSequence flag // this is inlined generation of the 0xFFFFFFFF flag for each input @@ -291,7 +291,7 @@ module { func(txin) { let sequence_buffer = VarArray.repeat(0, 4); Common.writeLE32(sequence_buffer, 0, txin.sequence); - Array.fromVarArray(sequence_buffer); + sequence_buffer.toArray(); } ); let sequences = sequences_buffer.flatten(); @@ -307,7 +307,7 @@ module { let input_index_buffer = VarArray.repeat(0, 4); Common.writeLE32(input_index_buffer, 0, txInputIndex); - let input_index = Array.fromVarArray(input_index_buffer); + let input_index = input_index_buffer.toArray(); // spend_type = (ext_flag * 2) + annex_present let (spend_type, scriptpath_bytes) : ([Nat8], [Nat8]) = switch (maybe_leaf_hash) { @@ -339,13 +339,13 @@ module { scriptpath_bytes, ].flatten(); - return Hash.taggedHash(data, "TapSighash"); + Hash.taggedHash(data, "TapSighash"); }; /// Serialize transaction to bytes with layout: /// `| version | witness flags if it is present | len(txIns) | txIns | len(txOuts) | txOuts | witnesses | locktime |` public func toBytes() : [Nat8] { - let has_non_empty_witness = Array.fromVarArray(witnesses).foldLeft( + let has_non_empty_witness = witnesses.toArray().foldLeft( false, func(accum, witness) { (witness.size() > 0) or accum; @@ -488,7 +488,7 @@ module { outputOffset += 4; assert (outputOffset == output.size()); - let result = Array.fromVarArray(output); + let result = output.toArray(); result; }; @@ -601,7 +601,7 @@ module { outputOffset += 4; assert (outputOffset == output.size()); - Array.fromVarArray(output); + output.toArray(); }; }; }; diff --git a/src/bitcoin/TxInput.mo b/src/bitcoin/TxInput.mo index 11f4b5b..6f8ca1c 100644 --- a/src/bitcoin/TxInput.mo +++ b/src/bitcoin/TxInput.mo @@ -1,12 +1,11 @@ -import Array "mo:core/Array"; import Blob "mo:core/Blob"; import { type Iter; type Result } "mo:core/Types"; import VarArray "mo:core/VarArray"; import ByteUtils "../ByteUtils"; import Common "../Common"; -import Script "./Script"; -import Types "./Types"; +import Script "Script"; +import Types "Types"; module { // Deserialize a TxInput from bytes with layout: @@ -79,7 +78,7 @@ module { outputOffset += 4; assert (outputOffset == output.size()); - return Array.fromVarArray(output); + output.toArray(); }; }; }; diff --git a/src/bitcoin/TxOutput.mo b/src/bitcoin/TxOutput.mo index 7ee42d7..cca1b7b 100644 --- a/src/bitcoin/TxOutput.mo +++ b/src/bitcoin/TxOutput.mo @@ -1,11 +1,10 @@ -import Array "mo:core/Array"; import { type Iter; type Result } "mo:core/Types"; import VarArray "mo:core/VarArray"; import ByteUtils "../ByteUtils"; import Common "../Common"; -import Script "./Script"; -import Types "./Types"; +import Script "Script"; +import Types "Types"; module { // Deserialize TxOutput from data with layout: @@ -40,7 +39,7 @@ module { Common.writeLE64(output, 0, amount); Common.copy(output, 8, encodedScript, 0, encodedScript.size()); - return Array.fromVarArray(output); + output.toArray(); }; }; }; diff --git a/src/bitcoin/Wif.mo b/src/bitcoin/Wif.mo index dcf0001..105d5be 100644 --- a/src/bitcoin/Wif.mo +++ b/src/bitcoin/Wif.mo @@ -3,7 +3,7 @@ import { type Iter; type Result } "mo:core/Types"; import Base58Check "../Base58Check"; import ByteUtils "../ByteUtils"; import Common "../Common"; -import Types "./Types"; +import Types "Types"; module { public type WifPrivateKey = Text; diff --git a/src/bitcoin/Witness.mo b/src/bitcoin/Witness.mo index 1041ae6..3b23353 100644 --- a/src/bitcoin/Witness.mo +++ b/src/bitcoin/Witness.mo @@ -1,7 +1,7 @@ import Array "mo:core/Array"; import List "mo:core/List"; -import { type Result; type Iter } "mo:core/Types"; import Nat "mo:core/Nat"; +import { type Result; type Iter } "mo:core/Types"; import VarArray "mo:core/VarArray"; import ByteUtils "../ByteUtils"; @@ -25,7 +25,7 @@ module { buffer.add(size); buffer.add(witness_element); }; - buffer.toArray().flatten(); + buffer.toArray().flatten(); }; @@ -52,7 +52,7 @@ module { }; witness[i] := witness_element; }; - let result = Array.fromVarArray(witness); + let result = witness.toArray(); #ok result; }; }; diff --git a/src/ec/Affine.mo b/src/ec/Affine.mo index 48882c5..973dc7d 100644 --- a/src/ec/Affine.mo +++ b/src/ec/Affine.mo @@ -1,9 +1,8 @@ -import Array "mo:core/Array"; import VarArray "mo:core/VarArray"; import Common "../Common"; -import Curves "./Curves"; -import FpBase "./Fp"; +import Curves "Curves"; +import FpBase "Fp"; module { type Fp = FpBase.Fp; @@ -111,12 +110,12 @@ module { let startByte : Nat8 = if (y.value % 2 == 0) 0x02 else 0x03; let output = VarArray.repeat(startByte, 33); Common.writeBE256(output, 1, x.value); - Array.fromVarArray(output); + output.toArray(); } else { let output = VarArray.repeat(0x04, 65); Common.writeBE256(output, 1, x.value); Common.writeBE256(output, 33, y.value); - Array.fromVarArray(output); + output.toArray(); }; }; }; diff --git a/src/ec/Curves.mo b/src/ec/Curves.mo index 72c9033..0e4f749 100644 --- a/src/ec/Curves.mo +++ b/src/ec/Curves.mo @@ -1,4 +1,4 @@ -import Fp "./Fp"; +import Fp "Fp"; module { public type Curve = { diff --git a/src/ec/Field.mo b/src/ec/Field.mo index 21a6e3a..acdb859 100644 --- a/src/ec/Field.mo +++ b/src/ec/Field.mo @@ -1,14 +1,14 @@ import Int "mo:core/Int"; import Nat "mo:core/Nat"; -import Numbers "./Numbers"; +import Numbers "Numbers"; module { // Compute a ** -1 mod n. public func inverse(a : Nat, n : Nat) : ?Nat { let (gcd, x, _) = Numbers.eea(a, n); - return if (gcd != 1) { + if (gcd != 1) { null; } else { let inverse = if (x < 0) x + n else x; @@ -39,7 +39,7 @@ module { public func add(a : Nat, b : Nat, n : Nat) : Nat { let sum = a + b; - return if (sum < n) { + if (sum < n) { sum; } else { sum - n; diff --git a/src/ec/Fp.mo b/src/ec/Fp.mo index e7fafff..f7c76e2 100644 --- a/src/ec/Fp.mo +++ b/src/ec/Fp.mo @@ -1,6 +1,6 @@ import Runtime "mo:core/Runtime"; -import Field "./Field"; +import Field "Field"; module { // Arithmetic computations modulo n over the given _value. diff --git a/src/ec/Jacobi.mo b/src/ec/Jacobi.mo index 8717c7f..8d4c2f7 100644 --- a/src/ec/Jacobi.mo +++ b/src/ec/Jacobi.mo @@ -10,10 +10,10 @@ import Int "mo:core/Int"; import Nat "mo:core/Nat"; import Runtime "mo:core/Runtime"; -import Affine "./Affine"; -import BaseFp "./Fp"; -import Curves "./Curves"; -import Numbers "./Numbers"; +import Affine "Affine"; +import Curves "Curves"; +import BaseFp "Fp"; +import Numbers "Numbers"; module { type Fp = BaseFp.Fp; @@ -416,7 +416,7 @@ module { func fpFromInt(value : Int, curve : Curves.Curve) : Fp { let mod : Int = value % curve.p; - return if (mod < 0) { + if (mod < 0) { curve.Fp(Int.abs(mod + curve.p)); } else { curve.Fp(Int.abs(mod)); diff --git a/src/ec/Numbers.mo b/src/ec/Numbers.mo index 09f4f73..86f98da 100644 --- a/src/ec/Numbers.mo +++ b/src/ec/Numbers.mo @@ -22,13 +22,13 @@ module { number /= 2; }; - return bitsBuffer.toArray(); + bitsBuffer.toArray(); }; // Convert given number to binary represented as an array of Bool. public func toBinary(a : Nat) : [Bool] { let reversedBinary = toBinaryReversed(a); - return Array.tabulate( + Array.tabulate( reversedBinary.size(), func(i) { reversedBinary[reversedBinary.size() - i - 1]; @@ -55,6 +55,6 @@ module { input /= 2; }; - return output.toArray(); + output.toArray(); }; }; diff --git a/src/ecdsa/Der.mo b/src/ecdsa/Der.mo index ecdef2e..fe67ec7 100644 --- a/src/ecdsa/Der.mo +++ b/src/ecdsa/Der.mo @@ -1,14 +1,13 @@ -import Array "mo:core/Array"; import Blob "mo:core/Blob"; import List "mo:core/List"; -import { type Result; type Iter } "mo:core/Types"; import Nat "mo:core/Nat"; import Nat8 "mo:core/Nat8"; +import { type Result; type Iter } "mo:core/Types"; import VarArray "mo:core/VarArray"; import ByteUtils "../ByteUtils"; import Common "../Common"; -import Types "./Types"; +import Types "Types"; module { type DerSignature = Types.DerSignature; @@ -91,7 +90,7 @@ module { output.add(i); }; - return Blob.fromArray(output.toArray()); + Blob.fromArray(output.toArray()); }; // Accepts a Blob containing the concatenation of the 32-byte big endian @@ -105,7 +104,7 @@ module { Common.copy(rdata, 0, data, 0, 32); Common.copy(sdata, 0, data, 32, 32); - return _encodeSignature(Array.fromVarArray(rdata), Array.fromVarArray(sdata)); + _encodeSignature(rdata.toArray(), sdata.toArray()); }; // Decode signature in DER format. @@ -182,7 +181,7 @@ module { for (i in Nat.range(0, Nat.min(rData.size(), 32))) { aligned[aligned.size() - 1 - i] := rData[rData.size() - 1 - i]; }; - Array.fromVarArray(aligned); + aligned.toArray(); } else { rData; }; @@ -193,7 +192,7 @@ module { for (i in Nat.range(0, Nat.min(sData.size(), 32))) { aligned[aligned.size() - 1 - i] := sData[sData.size() - 1 - i]; }; - Array.fromVarArray(aligned); + aligned.toArray(); } else { sData; }; diff --git a/src/ecdsa/Ecdsa.mo b/src/ecdsa/Ecdsa.mo index ca825e2..2926a7c 100644 --- a/src/ecdsa/Ecdsa.mo +++ b/src/ecdsa/Ecdsa.mo @@ -5,7 +5,7 @@ import Sha256 "mo:sha2/Sha256"; import Common "../Common"; import Fp "../ec/Fp"; import Jacobi "../ec/Jacobi"; -import Types "./Types"; +import Types "Types"; module { public type Signature = Types.Signature; diff --git a/src/ecdsa/Publickey.mo b/src/ecdsa/Publickey.mo index d817a23..48ef7ff 100644 --- a/src/ecdsa/Publickey.mo +++ b/src/ecdsa/Publickey.mo @@ -2,7 +2,7 @@ import { type Result } "mo:core/Types"; import Affine "../ec/Affine"; import Curves "../ec/Curves"; -import Types "./Types"; +import Types "Types"; module { type PublicKey = Types.PublicKey; @@ -66,6 +66,6 @@ module { compressed : Bool, ) : Types.Sec1PublicKey { let point : Affine.Point = #point(pk.coords.x, pk.coords.y, pk.curve); - return (Affine.toBytes(point, compressed), pk.curve); + (Affine.toBytes(point, compressed), pk.curve); }; }; diff --git a/test/Hex.mo b/test/Hex.mo index 7393b31..cda5721 100644 --- a/test/Hex.mo +++ b/test/Hex.mo @@ -2,7 +2,6 @@ import Nat "mo:core/Nat"; // Hex decoding imported from // https://github.com/aviate-labs/encoding.mo/blob/main/src/Hex.mo to // facilitate in testing. -import Array "mo:core/Array"; import VarArray "mo:core/VarArray"; import Char "mo:core/Char"; import Iter "mo:core/Iter"; @@ -75,6 +74,6 @@ module { }; }; }; - #ok(Array.fromVarArray(ns)); + #ok(ns.toArray()); }; }; diff --git a/test/bitcoin/bitcoin.test.mo b/test/bitcoin/bitcoin.test.mo index 09b30fb..e3a7f3c 100644 --- a/test/bitcoin/bitcoin.test.mo +++ b/test/bitcoin/bitcoin.test.mo @@ -5,7 +5,7 @@ import Runtime "mo:core/Runtime"; import Address "../../src/bitcoin/Address"; import Bitcoin "../../src/bitcoin/Bitcoin"; -import BitcoinTestTools "./bitcoinTestTools"; +import BitcoinTestTools "bitcoinTestTools"; import Hex "../Hex"; import Script "../../src/bitcoin/Script"; import TestUtils "../TestUtils"; diff --git a/test/bitcoin/bitcoinTestTools.mo b/test/bitcoin/bitcoinTestTools.mo index bc4e697..bd07b48 100644 --- a/test/bitcoin/bitcoinTestTools.mo +++ b/test/bitcoin/bitcoinTestTools.mo @@ -1,4 +1,3 @@ -import Array "mo:core/Array"; import Blob "mo:core/Blob"; import Int "mo:core/Int"; import Nat8 "mo:core/Nat8"; @@ -53,7 +52,7 @@ module { let encodedOutput : [var Nat8] = VarArray.repeat(0, 64); Common.writeBE256(encodedOutput, 0, signature.r); Common.writeBE256(encodedOutput, 32, signature.s); - return Blob.fromArray(Array.fromVarArray(encodedOutput)); + return Blob.fromArray(encodedOutput.toArray()); }; // Returns the public key associated to `bitcoinPrivateKey`. diff --git a/test/bitcoin/p2pkhSighash.test.mo b/test/bitcoin/p2pkhSighash.test.mo index c003c5c..ebbb93e 100644 --- a/test/bitcoin/p2pkhSighash.test.mo +++ b/test/bitcoin/p2pkhSighash.test.mo @@ -6,7 +6,7 @@ import Runtime "mo:core/Runtime"; import Hex "../Hex"; import Script "../../src/bitcoin/Script"; -import TestCases "./p2pkhSighashTestVectors"; +import TestCases "p2pkhSighashTestVectors"; import TestUtils "../TestUtils"; import Transaction "../../src/bitcoin/Transaction"; import Types "../../src/bitcoin/Types"; diff --git a/test/common.test.mo b/test/common.test.mo index b5a5a76..a426867 100644 --- a/test/common.test.mo +++ b/test/common.test.mo @@ -129,7 +129,7 @@ test( }, ); Common.writeBE32(output, 0, currentData.nat32); - assert (expected == Array.fromVarArray(output)); + assert (expected == output.toArray()); }; }, ); @@ -148,7 +148,7 @@ test( }, ); Common.writeBE64(output, 0, currentData.nat64); - assert (expected == Array.fromVarArray(output)); + assert (expected == output.toArray()); }; }, ); @@ -167,7 +167,7 @@ test( }, ); Common.writeBE128(output, 0, currentData.nat128); - assert (expected == Array.fromVarArray(output)); + assert (expected == output.toArray()); }; }, ); @@ -186,7 +186,7 @@ test( }, ); Common.writeBE256(output, 0, currentData.nat256); - assert (expected == Array.fromVarArray(output)); + assert (expected == output.toArray()); }; }, ); diff --git a/test/ec/jacobi.test.mo b/test/ec/jacobi.test.mo index ffec49f..55862d8 100644 --- a/test/ec/jacobi.test.mo +++ b/test/ec/jacobi.test.mo @@ -7,9 +7,9 @@ import Common "../../src/Common"; import Curves "../../src/ec/Curves"; import Hex "../Hex"; import Jacobi "../../src/ec/Jacobi"; -import Secp256k1TestVectors "./Secp256k1TestVectors"; +import Secp256k1TestVectors "Secp256k1TestVectors"; import TestUtils "../TestUtils"; -import WycheproofEcdhTestVectors "./wycheproofEcdhTestVectors"; +import WycheproofEcdhTestVectors "wycheproofEcdhTestVectors"; type DoublingVector = Secp256k1TestVectors.DoublingVector; type MultiplicationVector = Secp256k1TestVectors.MultiplicationVector; @@ -111,7 +111,7 @@ func testWycheproofEcdh(testCase : WycheproofEcdhTestCase) { for (i in Nat.range(0, Nat.min(privateKeyBytes.size(), 32))) { alignedPrivateKey[alignedPrivateKey.size() - 1 - i] := privateKeyBytes[privateKeyBytes.size() - 1 - i]; }; - let privateKey : Nat = Common.readBE256(Array.fromVarArray(alignedPrivateKey), 0); + let privateKey : Nat = Common.readBE256(alignedPrivateKey.toArray(), 0); // Read expected output. It is sometimes empty. let expectedOutput : ?Nat = if (outputBytes.size() > 0) { diff --git a/test/ecdsa/ecdsa.test.mo b/test/ecdsa/ecdsa.test.mo index 04a27ba..c594356 100644 --- a/test/ecdsa/ecdsa.test.mo +++ b/test/ecdsa/ecdsa.test.mo @@ -9,7 +9,7 @@ import Ecdsa "../../src/ecdsa/Ecdsa"; import Hex "../Hex"; import PublicKey "../../src/ecdsa/Publickey"; import TestUtils "../TestUtils"; -import WycheproofEcdsaTestVectors "./wycheproofEcdsaSecp256k1TestVectors"; +import WycheproofEcdsaTestVectors "wycheproofEcdsaSecp256k1TestVectors"; type WycheproofEcdsaTestCase = WycheproofEcdsaTestVectors.WycheproofEcdsaTestCase; let runTest = TestUtils.runTestWithDefaults; diff --git a/test/segwit.test.mo b/test/segwit.test.mo index 448d3ca..e63e663 100644 --- a/test/segwit.test.mo +++ b/test/segwit.test.mo @@ -1,4 +1,3 @@ -import Array "mo:core/Array"; import Char "mo:core/Char"; import List "mo:core/List"; import Nat "mo:core/Nat"; @@ -230,7 +229,7 @@ func testInvalidAddress(testCase : Text) { // Test whether address encoding fails on invalid input. func testInvalidAddressEncoding(testCase : InvalidAddressEncodingTestCase) { - let program = Array.fromVarArray(VarArray.repeat(0, testCase.programSize)); + let program = VarArray.repeat(0, testCase.programSize).toArray(); switch (Segwit.encode(testCase.hrp, { version = testCase.version; program })) { case (# ok(_)) { Runtime.trap("Encode succeeds on invalid input."); From 80ad9f4bcb45f0b569cff9cc356aac3903277698 Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Wed, 15 Apr 2026 12:36:38 +0200 Subject: [PATCH 02/34] Add bench.yml workflow back in --- .github/workflows/bench.yml | 58 +++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/workflows/bench.yml diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml new file mode 100644 index 0000000..c805840 --- /dev/null +++ b/.github/workflows/bench.yml @@ -0,0 +1,58 @@ +name: Benchmarks + +on: + push: + branches: [ main ] + pull_request: + +permissions: + contents: write + +jobs: + bench: + name: Run benches and commit markdown + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + persist-credentials: true + fetch-depth: 0 + ref: ${{ github.head_ref || github.ref_name }} + + - name: Install dfx and IC SDK + uses: dfinity/setup-dfx@main + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '22' + + - name: Install mops and PocketIC + run: | + npm install -g ic-mops @dfinity/pic + mops --version + + - name: Install dependencies (mops) + run: mops install + + - name: Setup Motoko toolchain (mops) + run: mops toolchain init + + - name: Run benches to markdown + run: | + mops bench > benchmark.md + + - name: Commit benchmark.md if changed + run: | + if git diff --quiet -- benchmark.md; then + echo "No changes in benchmark.md" + else + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + BRANCH_NAME="${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}" + git add benchmark.md + git commit -m "chore(bench): update benchmark.md [skip ci]" + git pull --rebase origin "$BRANCH_NAME" + git push origin HEAD:"$BRANCH_NAME" + fi From 658795f6973fe23948a6ada42f159127d56ccd52 Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Wed, 15 Apr 2026 12:44:40 +0200 Subject: [PATCH 03/34] Add empty benchmark.md --- benchmark.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 benchmark.md diff --git a/benchmark.md b/benchmark.md new file mode 100644 index 0000000..e69de29 From a3857f5ada0c500fef7d53049826d8cf3356359c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:46:29 +0000 Subject: [PATCH 04/34] chore(bench): update benchmark.md [skip ci] --- benchmark.md | 378 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 378 insertions(+) diff --git a/benchmark.md b/benchmark.md index e69de29..15bffa4 100644 --- a/benchmark.md +++ b/benchmark.md @@ -0,0 +1,378 @@ +# Benchmark Results + + + +
+ +bench/base58.bench.mo $({\color{gray}0\%})$ + +### Base58 encode/decode + +_Benchmark Base58 encode/decode across input sizes_ + + +Instructions: ${\color{gray}0\\%}$ +Heap: ${\color{gray}0\\%}$ +Stable Memory: ${\color{gray}0\\%}$ +Garbage Collection: ${\color{gray}0\\%}$ + + +**Instructions** + +| | len 0 | len 10 | len 32 | len 64 | len 128 | +| :----- | ----: | -----: | ------: | --------: | --------: | +| encode | 1_140 | 40_998 | 307_972 | 1_143_228 | 4_398_190 | +| decode | 1_316 | 36_139 | 300_171 | 1_131_407 | 4_379_041 | + + +**Heap** + +| | len 0 | len 10 | len 32 | len 64 | len 128 | +| :----- | ----: | -----: | -----: | -----: | ------: | +| encode | 272 B | 272 B | 272 B | 272 B | 272 B | +| decode | 272 B | 272 B | 272 B | 272 B | 272 B | + + +**Garbage Collection** + +| | len 0 | len 10 | len 32 | len 64 | len 128 | +| :----- | ----: | -----: | -------: | -------: | -------: | +| encode | 420 B | 860 B | 1.9 KiB | 3.45 KiB | 6.51 KiB | +| decode | 424 B | 580 B | 1.21 KiB | 2.14 KiB | 4 KiB | + + +
+ +
+ +bench/base58check.bench.mo $({\color{gray}0\%})$ + +### Base58Check encode/decode + +_Benchmark Base58Check encode/decode across input sizes_ + + +Instructions: ${\color{gray}0\\%}$ +Heap: ${\color{gray}0\\%}$ +Stable Memory: ${\color{gray}0\\%}$ +Garbage Collection: ${\color{gray}0\\%}$ + + +**Instructions** + +| | len 0 | len 10 | len 32 | len 64 | len 128 | +| :----- | -----: | ------: | ------: | --------: | --------: | +| encode | 46_726 | 109_577 | 429_302 | 1_345_449 | 4_760_681 | +| decode | 44_061 | 102_455 | 415_284 | 1_321_591 | 4_717_697 | + + +**Heap** + +| | len 0 | len 10 | len 32 | len 64 | len 128 | +| :----- | ----: | -----: | -----: | -----: | ------: | +| encode | 272 B | 272 B | 272 B | 272 B | 272 B | +| decode | 272 B | 272 B | 272 B | 272 B | 272 B | + + +**Garbage Collection** + +| | len 0 | len 10 | len 32 | len 64 | len 128 | +| :----- | -------: | -------: | -------: | -------: | --------: | +| encode | 4.08 KiB | 4.66 KiB | 5.89 KiB | 7.66 KiB | 11.25 KiB | +| decode | 3.94 KiB | 4.23 KiB | 4.95 KiB | 6 KiB | 8.13 KiB | + + +
+ +
+ +bench/bech32.bench.mo $({\color{gray}0\%})$ + +### Bech32 vs Bech32m + +_Compare Bech32 and Bech32m encoding across sizes_ + + +Instructions: ${\color{gray}0\\%}$ +Heap: ${\color{gray}0\\%}$ +Stable Memory: ${\color{gray}0\\%}$ +Garbage Collection: ${\color{gray}0\\%}$ + + +**Instructions** + +| | len 0 | len 5 | len 20 | len 32 | +| :------------- | -----: | -----: | -----: | -----: | +| encode bech32 | 18_277 | 24_053 | 41_341 | 55_225 | +| encode bech32m | 18_309 | 24_085 | 41_373 | 55_257 | +| decode bech32 | 11_354 | 17_414 | 35_029 | 49_153 | +| decode bech32m | 11_428 | 17_512 | 35_103 | 49_215 | + + +**Heap** + +| | len 0 | len 5 | len 20 | len 32 | +| :------------- | ----: | ----: | -----: | -----: | +| encode bech32 | 272 B | 272 B | 272 B | 272 B | +| encode bech32m | 272 B | 272 B | 272 B | 272 B | +| decode bech32 | 272 B | 272 B | 272 B | 272 B | +| decode bech32m | 272 B | 272 B | 272 B | 272 B | + + +**Garbage Collection** + +| | len 0 | len 5 | len 20 | len 32 | +| :------------- | -------: | -------: | -------: | -------: | +| encode bech32 | 1.16 KiB | 1.36 KiB | 1.94 KiB | 2.41 KiB | +| encode bech32m | 1.16 KiB | 1.36 KiB | 1.94 KiB | 2.41 KiB | +| decode bech32 | 824 B | 956 B | 1.24 KiB | 1.49 KiB | +| decode bech32m | 824 B | 956 B | 1.24 KiB | 1.49 KiB | + + +
+ +
+ +bench/bip32.bench.mo $({\color{gray}0\%})$ + +### BIP32 derivePath: text vs array + +_Compare path representations for public derivation_ + + +Instructions: ${\color{gray}0\\%}$ +Heap: ${\color{gray}0\\%}$ +Stable Memory: ${\color{gray}0\\%}$ +Garbage Collection: ${\color{gray}0\\%}$ + + +**Instructions** + +| | depth 3 | depth 4 | depth 5 | +| :---- | ----------: | ----------: | ----------: | +| text | 544_380_979 | 725_189_387 | 909_632_280 | +| array | 544_277_099 | 725_124_043 | 909_557_333 | + + +**Heap** + +| | depth 3 | depth 4 | depth 5 | +| :---- | ------: | ------: | ------: | +| text | 272 B | 272 B | 272 B | +| array | 272 B | 272 B | 272 B | + + +**Garbage Collection** + +| | depth 3 | depth 4 | depth 5 | +| :---- | --------: | --------: | --------: | +| text | 13.37 MiB | 17.81 MiB | 22.35 MiB | +| array | 13.37 MiB | 17.81 MiB | 22.34 MiB | + + +
+ +
+ +bench/bitcoin_tx.bench.mo $({\color{gray}0\%})$ + +### Bitcoin tx: build vs sighash + +_Compare building a simple tx vs computing P2PKH sighash_ + + +Instructions: ${\color{gray}0\\%}$ +Heap: ${\color{gray}0\\%}$ +Stable Memory: ${\color{gray}0\\%}$ +Garbage Collection: ${\color{gray}0\\%}$ + + +**Instructions** + +| | 2 utxos | 4 utxos | +| :------ | --------: | --------: | +| build | 723_422 | 731_490 | +| sighash | 1_326_847 | 1_334_915 | + + +**Heap** + +| | 2 utxos | 4 utxos | +| :------ | ------: | ------: | +| build | 272 B | 272 B | +| sighash | 272 B | 272 B | + + +**Garbage Collection** + +| | 2 utxos | 4 utxos | +| :------ | --------: | --------: | +| build | 14.39 KiB | 14.86 KiB | +| sighash | 30.39 KiB | 30.86 KiB | + + +
+ +
+ +bench/ec_arith.bench.mo $({\color{gray}0\%})$ + +### EC scalar mul: base vs arbitrary point + +_Compare scalar multiplication using generator vs arbitrary point_ + + +Instructions: ${\color{gray}0\\%}$ +Heap: ${\color{gray}0\\%}$ +Stable Memory: ${\color{gray}0\\%}$ +Garbage Collection: ${\color{gray}0\\%}$ + + +**Instructions** + +| | k small | k medium | k large | +| :------- | ---------: | ---------: | ---------: | +| mulBase | 10_058_884 | 17_922_235 | 36_223_854 | +| mulPoint | 10_053_853 | 17_917_924 | 36_218_103 | + + +**Heap** + +| | k small | k medium | k large | +| :------- | ------: | -------: | ------: | +| mulBase | 272 B | 272 B | 272 B | +| mulPoint | 272 B | 272 B | 272 B | + + +**Garbage Collection** + +| | k small | k medium | k large | +| :------- | ---------: | ---------: | ---------: | +| mulBase | 322.99 KiB | 513.06 KiB | 976.04 KiB | +| mulPoint | 322.36 KiB | 512.43 KiB | 975.4 KiB | + + +
+ +
+ +bench/ecdsa_verify.bench.mo $({\color{gray}0\%})$ + +### ECDSA verify: DER vs raw (DER decode cost) + +_Compare verifying using DER decode per run vs reusing parsed signature_ + + +Instructions: ${\color{gray}0\\%}$ +Heap: ${\color{gray}0\\%}$ +Stable Memory: ${\color{gray}0\\%}$ +Garbage Collection: ${\color{gray}0\\%}$ + + +**Instructions** + +| | sample 0 | sample 1 | +| :----------------- | ----------: | ----------: | +| DER+verify | 307_363_770 | 306_601_794 | +| verify (preparsed) | 306_817_755 | 306_100_633 | + + +**Heap** + +| | sample 0 | sample 1 | +| :----------------- | -------: | -------: | +| DER+verify | 272 B | 272 B | +| verify (preparsed) | 272 B | 272 B | + + +**Garbage Collection** + +| | sample 0 | sample 1 | +| :----------------- | -------: | -------: | +| DER+verify | 7.69 MiB | 7.66 MiB | +| verify (preparsed) | 7.67 MiB | 7.65 MiB | + + +
+ +
+ +bench/hash_hmac.bench.mo $({\color{gray}0\%})$ + +### HMAC: SHA256 vs SHA512 + +_Compare HMAC-SHA256 and HMAC-SHA512 across message sizes_ + + +Instructions: ${\color{gray}0\\%}$ +Heap: ${\color{gray}0\\%}$ +Stable Memory: ${\color{gray}0\\%}$ +Garbage Collection: ${\color{gray}0\\%}$ + + +**Instructions** + +| | len 0 | len 32 | len 64 | len 256 | +| :---------- | ------: | ------: | ------: | ------: | +| HMAC-SHA256 | 75_746 | 79_240 | 86_915 | 118_397 | +| HMAC-SHA512 | 118_026 | 121_255 | 123_599 | 152_298 | + + +**Heap** + +| | len 0 | len 32 | len 64 | len 256 | +| :---------- | ----: | -----: | -----: | ------: | +| HMAC-SHA256 | 272 B | 272 B | 272 B | 272 B | +| HMAC-SHA512 | 272 B | 272 B | 272 B | 272 B | + + +**Garbage Collection** + +| | len 0 | len 32 | len 64 | len 256 | +| :---------- | -------: | -------: | -------: | -------: | +| HMAC-SHA256 | 4.73 KiB | 4.73 KiB | 4.73 KiB | 4.73 KiB | +| HMAC-SHA512 | 6.78 KiB | 6.92 KiB | 6.97 KiB | 6.88 KiB | + + +
+ +
+ +bench/segwit.bench.mo $({\color{gray}0\%})$ + +### SegWit (address encode/decode) + +_Benchmark SegWit Bech32/Bech32m address encode/decode for common versions and program lengths_ + + +Instructions: ${\color{gray}0\\%}$ +Heap: ${\color{gray}0\\%}$ +Stable Memory: ${\color{gray}0\\%}$ +Garbage Collection: ${\color{gray}0\\%}$ + + +**Instructions** + +| | bc v0/20 | bc v0/32 | bc v1/32 | tb v0/20 | tb v0/32 | tb v1/32 | +| :----- | -------: | -------: | -------: | -------: | -------: | -------: | +| encode | 179_819 | 254_093 | 254_065 | 179_755 | 254_137 | 254_125 | +| decode | 82_458 | 118_960 | 118_951 | 82_426 | 119_000 | 118_975 | + + +**Heap** + +| | bc v0/20 | bc v0/32 | bc v1/32 | tb v0/20 | tb v0/32 | tb v1/32 | +| :----- | -------: | -------: | -------: | -------: | -------: | -------: | +| encode | 272 B | 272 B | 272 B | 272 B | 272 B | 272 B | +| decode | 272 B | 272 B | 272 B | 272 B | 272 B | 272 B | + + +**Garbage Collection** + +| | bc v0/20 | bc v0/32 | bc v1/32 | tb v0/20 | tb v0/32 | tb v1/32 | +| :----- | -------: | -------: | -------: | -------: | -------: | -------: | +| encode | 5.29 KiB | 6.76 KiB | 6.76 KiB | 5.29 KiB | 6.76 KiB | 6.76 KiB | +| decode | 2.23 KiB | 2.75 KiB | 2.75 KiB | 2.23 KiB | 2.75 KiB | 2.75 KiB | + + +
From 30500caf063dd2cb942f63c37170569574c0d3a3 Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Wed, 15 Apr 2026 13:34:26 +0200 Subject: [PATCH 05/34] Optimization by skill file --- src/Base58.mo | 34 ++++++++++++++++++++-------------- src/Bech32.mo | 23 ++++++++++++++--------- src/ByteUtils.mo | 6 +++--- src/Common.mo | 6 +++--- src/Segwit.mo | 3 ++- 5 files changed, 42 insertions(+), 30 deletions(-) diff --git a/src/Base58.mo b/src/Base58.mo index d75386c..7634e33 100755 --- a/src/Base58.mo +++ b/src/Base58.mo @@ -1,7 +1,10 @@ import Array "mo:core/Array"; +import Blob "mo:core/Blob"; import Char "mo:core/Char"; +import Nat16 "mo:core/Nat16"; import Nat32 "mo:core/Nat32"; import Nat8 "mo:core/Nat8"; +import Runtime "mo:core/Runtime"; import Text "mo:core/Text"; import { type Iter } "mo:core/Types"; import VarArray "mo:core/VarArray"; @@ -9,11 +12,11 @@ import VarArray "mo:core/VarArray"; module { // All alphanumeric characters except for "0", "I", "O", and "l". // prettier-ignore - private let base58Alphabet : [Char] = [ - '1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','G', - 'H','J','K','L','M','N','P','Q','R','S','T','U','V','W','X','Y','Z', - 'a','b','c','d','e','f','g','h','i','j','k','m','n','o','p','q','r', - 's','t','u','v','w','x','y','z' + private let base58Alphabet : [Nat8] = [ + 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70, 71, + 72, 74, 75, 76, 77, 78, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, + 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 109, 110, 111, 112, 113, 114, + 115, 116, 117, 118, 119, 120, 121, 122 ]; // prettier-ignore @@ -87,15 +90,15 @@ module { break l; }; case (?value) { - var carry : Nat = mapBase58[value.toNat32().toNat()].toNat(); + var carry : Nat32 = mapBase58[value.toNat32().toNat()].toNat16().toNat32(); assert (carry != 0xff); var i : Nat = 0; var b256Pointer : Nat = b256.size() - 1; label reverseIter while (carry != 0 or i < length) { - carry += 58 * b256[b256Pointer].toNat(); - b256[b256Pointer] := Nat8.fromNat(carry % 256); + carry += 58 * b256[b256Pointer].toNat16().toNat32(); + b256[b256Pointer] := Nat8.fromNat((carry % 256).toNat()); carry /= 256; i += 1; @@ -164,13 +167,13 @@ module { let b58 : [var Nat8] = VarArray.repeat(0, size); while (inputPointer < input.size()) { - var carry : Nat = input[inputPointer].toNat(); + var carry : Nat32 = input[inputPointer].toNat16().toNat32(); var i : Nat = 0; // Apply "b58 = b58 * 256 + ch". var b58Pointer : Nat = b58.size() - 1; label reverseIter while (carry != 0 or i < length) { - carry += 256 * (b58[b58Pointer]).toNat(); - b58[b58Pointer] := Nat8.fromNat(carry % 58); + carry += 256 * b58[b58Pointer].toNat16().toNat32(); + b58[b58Pointer] := Nat8.fromNat((carry % 58).toNat()); carry /= 58; i += 1; if (b58Pointer == 0) { @@ -187,16 +190,19 @@ module { var b58Pointer : Nat = size - length; while (b58Pointer < b58.size() and b58[b58Pointer] == 0) { b58Pointer += 1 }; - let output = Array.tabulate( + let outputBytes = Array.tabulate( zeroes + b58.size() - b58Pointer, func(i) { if (i < zeroes) { - Char.fromNat32(0x31); + 0x31 : Nat8; } else { base58Alphabet[b58[i + b58Pointer - zeroes].toNat()]; }; }, ); - Text.fromIter(output.values()); + switch (Blob.fromArray(outputBytes).decodeUtf8()) { + case (?t) t; + case null Runtime.trap("unreachable"); + }; }; }; diff --git a/src/Bech32.mo b/src/Bech32.mo index b1b48ff..a1e2148 100644 --- a/src/Bech32.mo +++ b/src/Bech32.mo @@ -2,8 +2,10 @@ import Array "mo:core/Array"; import Blob "mo:core/Blob"; import Char "mo:core/Char"; import Nat "mo:core/Nat"; +import Nat16 "mo:core/Nat16"; import Nat32 "mo:core/Nat32"; import Nat8 "mo:core/Nat8"; +import Runtime "mo:core/Runtime"; import Text "mo:core/Text"; import { type Result } "mo:core/Types"; import VarArray "mo:core/VarArray"; @@ -28,10 +30,10 @@ module { let CHARS_HIGHLIMIT : Nat8 = 0x7e; // prettier-ignore - let charset : [Char] = [ - 'q', 'p', 'z', 'r', 'y', '9', 'x', '8', 'g', 'f', '2', 't', 'v', 'd', 'w', - '0', 's', '3', 'j', 'n', '5', '4', 'k', 'h', 'c', 'e', '6', 'm', 'u', 'a', - '7', 'l' + let charset : [Nat8] = [ + 0x71, 0x70, 0x7a, 0x72, 0x79, 0x39, 0x78, 0x38, 0x67, 0x66, 0x32, 0x74, 0x76, 0x64, 0x77, + 0x30, 0x73, 0x33, 0x6a, 0x6e, 0x35, 0x34, 0x6b, 0x68, 0x63, 0x65, 0x36, 0x6d, 0x75, 0x61, + 0x37, 0x6c ]; // Mapping from ASCII to indices in charset for characters that exist in @@ -62,16 +64,19 @@ module { let checksum : [Nat8] = createChecksum(encodedHrp, values, encoding); // hrp | '1' | values | checksum. - let output : [Char] = [ - hrp.toArray(), - ['1'], + let output : [Nat8] = [ + encodedHrp, + [0x31] : [Nat8], values.map(func x = charset[x.toNat()]), checksum.map(func x = charset[x.toNat()]), ].flatten(); assert output.size() <= 90; - Text.fromArray(output); + switch (Blob.fromArray(output).decodeUtf8()) { + case (?t) t; + case null Runtime.trap("unreachable"); + }; }; // Decode given text as Bech32 or Bech32m. @@ -226,7 +231,7 @@ module { for (value in values.values()) { let c0 : Nat8 = Nat8.fromIntWrap((c >> 25).toNat()); - c := ((c & 0x1ffffff) << 5) ^ Nat32.fromIntWrap(value.toNat()); + c := ((c & 0x1ffffff) << 5) ^ value.toNat16().toNat32(); // Conditionally add in coefficients of the generator polynomial. if (c0 & 1 > 0) c ^= 0x3b6a57b2; diff --git a/src/ByteUtils.mo b/src/ByteUtils.mo index 67898f4..d2cb751 100644 --- a/src/ByteUtils.mo +++ b/src/ByteUtils.mo @@ -46,7 +46,7 @@ module { public func readLE16(data : Iter) : ?Nat16 { do ? { let (a, b) = (data.next()!, data.next()!); - Nat16.fromIntWrap(b.toNat()) << 8 | Nat16.fromIntWrap(a.toNat()); + b.toNat16() << 8 | a.toNat16(); }; }; @@ -55,7 +55,7 @@ module { public func readLE32(data : Iter) : ?Nat32 { do ? { let (a, b, c, d) = (data.next()!, data.next()!, data.next()!, data.next()!); - Nat32.fromIntWrap(d.toNat()) << 24 | Nat32.fromIntWrap(c.toNat()) << 16 | Nat32.fromIntWrap(b.toNat()) << 8 | Nat32.fromIntWrap(a.toNat()); + d.toNat16().toNat32() << 24 | c.toNat16().toNat32() << 16 | b.toNat16().toNat32() << 8 | a.toNat16().toNat32(); }; }; @@ -74,7 +74,7 @@ module { data.next()!, ); - Nat64.fromIntWrap(h.toNat()) << 56 | Nat64.fromIntWrap(g.toNat()) << 48 | Nat64.fromIntWrap(f.toNat()) << 40 | Nat64.fromIntWrap(e.toNat()) << 32 | Nat64.fromIntWrap(d.toNat()) << 24 | Nat64.fromIntWrap(c.toNat()) << 16 | Nat64.fromIntWrap(b.toNat()) << 8 | Nat64.fromIntWrap(a.toNat()); + h.toNat16().toNat32().toNat64() << 56 | g.toNat16().toNat32().toNat64() << 48 | f.toNat16().toNat32().toNat64() << 40 | e.toNat16().toNat32().toNat64() << 32 | d.toNat16().toNat32().toNat64() << 24 | c.toNat16().toNat32().toNat64() << 16 | b.toNat16().toNat32().toNat64() << 8 | a.toNat16().toNat32().toNat64(); }; }; diff --git a/src/Common.mo b/src/Common.mo index ef28ca5..1dd3e79 100644 --- a/src/Common.mo +++ b/src/Common.mo @@ -7,7 +7,7 @@ import Nat8 "mo:core/Nat8"; module { // Read big endian 32-bit natural number starting at offset. public func readBE32(bytes : [Nat8], offset : Nat) : Nat32 { - Nat32.fromIntWrap(bytes[offset + 0].toNat()) << 24 | Nat32.fromIntWrap(bytes[offset + 1].toNat()) << 16 | Nat32.fromIntWrap(bytes[offset + 2].toNat()) << 8 | Nat32.fromIntWrap(bytes[offset + 3].toNat()); + bytes[offset + 0].toNat16().toNat32() << 24 | bytes[offset + 1].toNat16().toNat32() << 16 | bytes[offset + 2].toNat16().toNat32() << 8 | bytes[offset + 3].toNat16().toNat32(); }; // Read big endian 64-bit natural number starting at offset. @@ -15,7 +15,7 @@ module { let first : Nat32 = readBE32(bytes, offset); let second : Nat32 = readBE32(bytes, offset + 4); - Nat64.fromIntWrap(first.toNat()) << 32 | Nat64.fromIntWrap(second.toNat()); + first.toNat64() << 32 | second.toNat64(); }; // Read big endian 128-bit natural number starting at offset. @@ -71,7 +71,7 @@ module { // Read little endian 32-bit natural number starting at offset. public func readLE32(bytes : [Nat8], offset : Nat) : Nat32 { - Nat32.fromIntWrap(bytes[offset + 3].toNat()) << 24 | Nat32.fromIntWrap(bytes[offset + 2].toNat()) << 16 | Nat32.fromIntWrap(bytes[offset + 1].toNat()) << 8 | Nat32.fromIntWrap(bytes[offset + 0].toNat()); + bytes[offset + 3].toNat16().toNat32() << 24 | bytes[offset + 2].toNat16().toNat32() << 16 | bytes[offset + 1].toNat16().toNat32() << 8 | bytes[offset + 0].toNat16().toNat32(); }; // Write given value as 16-bit little endian into array starting at offset. diff --git a/src/Segwit.mo b/src/Segwit.mo index 7eb85bc..3796f1e 100644 --- a/src/Segwit.mo +++ b/src/Segwit.mo @@ -1,5 +1,6 @@ import List "mo:core/List"; import Nat "mo:core/Nat"; +import Nat16 "mo:core/Nat16"; import Nat32 "mo:core/Nat32"; import Nat8 "mo:core/Nat8"; import Runtime "mo:core/Runtime"; @@ -126,7 +127,7 @@ module { let maxv : Nat32 = (1 << to) - 1; for (value in data) { - let v : Nat32 = Nat32.fromIntWrap(value.toNat()); + let v : Nat32 = value.toNat16().toNat32(); if ((v >> from) != 0) { return #err("Invalid input value: " # value.toNat().toText()); From bb6aac361f7268f19240977ad7207d55ea1fd75e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:43:18 +0000 Subject: [PATCH 06/34] chore(bench): update benchmark.md [skip ci] --- benchmark.md | 74 ++++++++++++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/benchmark.md b/benchmark.md index 15bffa4..ed3a81a 100644 --- a/benchmark.md +++ b/benchmark.md @@ -19,10 +19,10 @@ Garbage Collection: ${\color{gray}0\\%}$ **Instructions** -| | len 0 | len 10 | len 32 | len 64 | len 128 | -| :----- | ----: | -----: | ------: | --------: | --------: | -| encode | 1_140 | 40_998 | 307_972 | 1_143_228 | 4_398_190 | -| decode | 1_316 | 36_139 | 300_171 | 1_131_407 | 4_379_041 | +| | len 0 | len 10 | len 32 | len 64 | len 128 | +| :----- | ----: | -----: | ------: | ------: | --------: | +| encode | 1_182 | 26_059 | 210_836 | 800_098 | 3_114_843 | +| decode | 1_316 | 26_644 | 213_296 | 805_615 | 3_127_486 | **Heap** @@ -35,10 +35,10 @@ Garbage Collection: ${\color{gray}0\\%}$ **Garbage Collection** -| | len 0 | len 10 | len 32 | len 64 | len 128 | -| :----- | ----: | -----: | -------: | -------: | -------: | -| encode | 420 B | 860 B | 1.9 KiB | 3.45 KiB | 6.51 KiB | -| decode | 424 B | 580 B | 1.21 KiB | 2.14 KiB | 4 KiB | +| | len 0 | len 10 | len 32 | len 64 | len 128 | +| :----- | ----: | -----: | -----: | -------: | -------: | +| encode | 372 B | 508 B | 808 B | 1.22 KiB | 2.07 KiB | +| decode | 424 B | 500 B | 676 B | 932 B | 1.41 KiB | @@ -60,10 +60,10 @@ Garbage Collection: ${\color{gray}0\\%}$ **Instructions** -| | len 0 | len 10 | len 32 | len 64 | len 128 | -| :----- | -----: | ------: | ------: | --------: | --------: | -| encode | 46_726 | 109_577 | 429_302 | 1_345_449 | 4_760_681 | -| decode | 44_061 | 102_455 | 415_284 | 1_321_591 | 4_717_697 | +| | len 0 | len 10 | len 32 | len 64 | len 128 | +| :----- | -----: | -----: | ------: | ------: | --------: | +| encode | 41_565 | 84_651 | 309_351 | 961_324 | 3_398_416 | +| decode | 42_174 | 83_950 | 306_560 | 955_606 | 3_388_120 | **Heap** @@ -76,10 +76,10 @@ Garbage Collection: ${\color{gray}0\\%}$ **Garbage Collection** -| | len 0 | len 10 | len 32 | len 64 | len 128 | -| :----- | -------: | -------: | -------: | -------: | --------: | -| encode | 4.08 KiB | 4.66 KiB | 5.89 KiB | 7.66 KiB | 11.25 KiB | -| decode | 3.94 KiB | 4.23 KiB | 4.95 KiB | 6 KiB | 8.13 KiB | +| | len 0 | len 10 | len 32 | len 64 | len 128 | +| :----- | -------: | -------: | -------: | -------: | -------: | +| encode | 3.91 KiB | 4.16 KiB | 4.63 KiB | 5.3 KiB | 6.66 KiB | +| decode | 3.94 KiB | 4.05 KiB | 4.31 KiB | 4.69 KiB | 5.44 KiB | @@ -103,10 +103,10 @@ Garbage Collection: ${\color{gray}0\\%}$ | | len 0 | len 5 | len 20 | len 32 | | :------------- | -----: | -----: | -----: | -----: | -| encode bech32 | 18_277 | 24_053 | 41_341 | 55_225 | -| encode bech32m | 18_309 | 24_085 | 41_373 | 55_257 | -| decode bech32 | 11_354 | 17_414 | 35_029 | 49_153 | -| decode bech32m | 11_428 | 17_512 | 35_103 | 49_215 | +| encode bech32 | 11_982 | 15_543 | 25_974 | 34_330 | +| encode bech32m | 12_014 | 15_575 | 26_006 | 34_362 | +| decode bech32 | 10_694 | 15_066 | 27_559 | 37_597 | +| decode bech32m | 10_768 | 15_164 | 27_633 | 37_659 | **Heap** @@ -121,12 +121,12 @@ Garbage Collection: ${\color{gray}0\\%}$ **Garbage Collection** -| | len 0 | len 5 | len 20 | len 32 | -| :------------- | -------: | -------: | -------: | -------: | -| encode bech32 | 1.16 KiB | 1.36 KiB | 1.94 KiB | 2.41 KiB | -| encode bech32m | 1.16 KiB | 1.36 KiB | 1.94 KiB | 2.41 KiB | -| decode bech32 | 824 B | 956 B | 1.24 KiB | 1.49 KiB | -| decode bech32m | 824 B | 956 B | 1.24 KiB | 1.49 KiB | +| | len 0 | len 5 | len 20 | len 32 | +| :------------- | ----: | ----: | -------: | -------: | +| encode bech32 | 840 B | 908 B | 1.09 KiB | 1.26 KiB | +| encode bech32m | 840 B | 908 B | 1.09 KiB | 1.26 KiB | +| decode bech32 | 804 B | 932 B | 1.2 KiB | 1.44 KiB | +| decode bech32m | 804 B | 932 B | 1.2 KiB | 1.44 KiB | @@ -150,8 +150,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | depth 3 | depth 4 | depth 5 | | :---- | ----------: | ----------: | ----------: | -| text | 544_380_979 | 725_189_387 | 909_632_280 | -| array | 544_277_099 | 725_124_043 | 909_557_333 | +| text | 544_358_509 | 725_157_063 | 909_593_270 | +| array | 544_254_818 | 725_092_034 | 909_518_890 | **Heap** @@ -166,7 +166,7 @@ Garbage Collection: ${\color{gray}0\\%}$ | | depth 3 | depth 4 | depth 5 | | :---- | --------: | --------: | --------: | -| text | 13.37 MiB | 17.81 MiB | 22.35 MiB | +| text | 13.37 MiB | 17.81 MiB | 22.34 MiB | | array | 13.37 MiB | 17.81 MiB | 22.34 MiB | @@ -191,8 +191,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | 2 utxos | 4 utxos | | :------ | --------: | --------: | -| build | 723_422 | 731_490 | -| sighash | 1_326_847 | 1_334_915 | +| build | 574_019 | 582_087 | +| sighash | 1_127_651 | 1_135_719 | **Heap** @@ -273,8 +273,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | sample 0 | sample 1 | | :----------------- | ----------: | ----------: | -| DER+verify | 307_363_770 | 306_601_794 | -| verify (preparsed) | 306_817_755 | 306_100_633 | +| DER+verify | 307_343_230 | 306_581_317 | +| verify (preparsed) | 306_815_024 | 306_097_902 | **Heap** @@ -355,8 +355,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | bc v0/20 | bc v0/32 | bc v1/32 | tb v0/20 | tb v0/32 | tb v1/32 | | :----- | -------: | -------: | -------: | -------: | -------: | -------: | -| encode | 179_819 | 254_093 | 254_065 | 179_755 | 254_137 | 254_125 | -| decode | 82_458 | 118_960 | 118_951 | 82_426 | 119_000 | 118_975 | +| encode | 146_027 | 204_067 | 204_039 | 145_963 | 204_111 | 204_099 | +| decode | 70_224 | 99_696 | 99_687 | 70_192 | 99_736 | 99_711 | **Heap** @@ -371,8 +371,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | bc v0/20 | bc v0/32 | bc v1/32 | tb v0/20 | tb v0/32 | tb v1/32 | | :----- | -------: | -------: | -------: | -------: | -------: | -------: | -| encode | 5.29 KiB | 6.76 KiB | 6.76 KiB | 5.29 KiB | 6.76 KiB | 6.76 KiB | -| decode | 2.23 KiB | 2.75 KiB | 2.75 KiB | 2.23 KiB | 2.75 KiB | 2.75 KiB | +| encode | 4.06 KiB | 5 KiB | 5 KiB | 4.06 KiB | 5 KiB | 5 KiB | +| decode | 2.18 KiB | 2.68 KiB | 2.68 KiB | 2.18 KiB | 2.68 KiB | 2.68 KiB | From a39fa8e46ec0c9bfd5d881ac0d976b811e32e22b Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Wed, 15 Apr 2026 14:47:15 +0200 Subject: [PATCH 07/34] Remove unused Char import --- src/Bech32.mo | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Bech32.mo b/src/Bech32.mo index a1e2148..08b7000 100644 --- a/src/Bech32.mo +++ b/src/Bech32.mo @@ -1,6 +1,5 @@ import Array "mo:core/Array"; import Blob "mo:core/Blob"; -import Char "mo:core/Char"; import Nat "mo:core/Nat"; import Nat16 "mo:core/Nat16"; import Nat32 "mo:core/Nat32"; From 9b4c80f9403fd65f492f12b30b38d2c39dadbb08 Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Thu, 16 Apr 2026 14:50:09 +0200 Subject: [PATCH 08/34] Optimize existing base58.decode algorithm --- src/Base58.mo | 57 +++++++++++++++++++-------------------------------- 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/src/Base58.mo b/src/Base58.mo index 7634e33..fee33fd 100755 --- a/src/Base58.mo +++ b/src/Base58.mo @@ -20,7 +20,7 @@ module { ]; // prettier-ignore - private let mapBase58 : [Nat8] = [ + private let mapBase58 : [Nat16] = [ 255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255, 255, 0, @@ -40,20 +40,17 @@ module { ]; // Convert the given Base58 input to Base256. - public func decode(input : Text) : [Nat8] { - let inputIter : Iter = input.chars(); - var current : ?Char = inputIter.next(); + public func decode(input_ : Text) : [Nat8] { + let input = Text.encodeUtf8(input_); + let inputIter : Iter = input.values(); + var current : ?Nat8 = inputIter.next(); var spaces : Nat = 0; // Skip leading spaces label l loop { switch (current) { - case (?' ') { - spaces := spaces + 1; - }; - case (_) { - break l; - }; + case (?0x20) spaces += 1; + case (_) break l; }; current := inputIter.next(); }; @@ -64,12 +61,8 @@ module { label l loop { switch (current) { - case (?'1') { - zeroes := zeroes + 1; - }; - case (_) { - break l; - }; + case (?0x31) zeroes += 1; + case (_) break l; }; current := inputIter.next(); }; @@ -79,32 +72,26 @@ module { // Base256, which is approximately 733 / 1000. The input size is multiplied // by this value and rounded up to get the total Base256 required size. let size : Nat = (input.size() - zeroes - spaces) * 733 / 1000 + 1; - let b256 : [var Nat8] = VarArray.repeat(0x00, size); + let b256 : [var Nat16] = VarArray.repeat(0x00, size); label l loop { switch (current) { - case (?' ') { - break l; - }; - case (null) { - break l; - }; + case (?0x20) break l; + case (null) break l; case (?value) { - var carry : Nat32 = mapBase58[value.toNat32().toNat()].toNat16().toNat32(); + var carry : Nat16 = mapBase58[value.toNat()]; assert (carry != 0xff); var i : Nat = 0; - var b256Pointer : Nat = b256.size() - 1; + var b256Pointer : Nat = size - 1; label reverseIter while (carry != 0 or i < length) { - carry += 58 * b256[b256Pointer].toNat16().toNat32(); - b256[b256Pointer] := Nat8.fromNat((carry % 256).toNat()); - carry /= 256; + carry +%= 58 * b256[b256Pointer]; + b256[b256Pointer] := (carry & 0xff); + carry >>= 8; i += 1; - if (b256Pointer == 0) { - break reverseIter; - }; + if (b256Pointer == 0) break reverseIter; b256Pointer -= 1; }; @@ -118,10 +105,8 @@ module { // Skip trailing spaces. label l loop { switch (current) { - case (?' ') {}; - case (_) { - break l; - }; + case (?0x20) {}; + case (_) break l; }; current := inputIter.next(); }; @@ -141,7 +126,7 @@ module { if (i < zeroes) { 0x00; } else { - b256[i + b256Pointer - zeroes]; + b256[i + b256Pointer - zeroes].toNat8(); }; }, ); From 7db8997f4abcdcb8af970cc3f8b8b0b183ec8a1a Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Thu, 16 Apr 2026 15:32:24 +0200 Subject: [PATCH 09/34] Rewrite Base58.decode based on positional access instead of iter Better readability --- src/Base58.mo | 87 +++++++++++++++++++-------------------------------- 1 file changed, 33 insertions(+), 54 deletions(-) diff --git a/src/Base58.mo b/src/Base58.mo index fee33fd..6f01f30 100755 --- a/src/Base58.mo +++ b/src/Base58.mo @@ -1,12 +1,10 @@ import Array "mo:core/Array"; import Blob "mo:core/Blob"; -import Char "mo:core/Char"; import Nat16 "mo:core/Nat16"; import Nat32 "mo:core/Nat32"; import Nat8 "mo:core/Nat8"; import Runtime "mo:core/Runtime"; import Text "mo:core/Text"; -import { type Iter } "mo:core/Types"; import VarArray "mo:core/VarArray"; module { @@ -42,77 +40,58 @@ module { // Convert the given Base58 input to Base256. public func decode(input_ : Text) : [Nat8] { let input = Text.encodeUtf8(input_); - let inputIter : Iter = input.values(); - var current : ?Nat8 = inputIter.next(); - var spaces : Nat = 0; - - // Skip leading spaces - label l loop { - switch (current) { - case (?0x20) spaces += 1; - case (_) break l; - }; - current := inputIter.next(); + let inputSize = input.size(); + var pos : Nat = 0; + + // Skip leading spaces. + while (pos < inputSize and input[pos] == 0x20) { + pos += 1; }; // Skip and count leading '1's. - var zeroes : Nat = 0; - var length : Nat = 0; + let start = pos; - label l loop { - switch (current) { - case (?0x31) zeroes += 1; - case (_) break l; - }; - current := inputIter.next(); + while (pos < inputSize and input[pos] == 0x31) { + pos += 1; }; + let zeroes : Nat = pos - start; // Compute how many bytes are needed for the Base256 representation. We // need log(58) / log(256) of one byte to represent a Base58 digit in // Base256, which is approximately 733 / 1000. The input size is multiplied // by this value and rounded up to get the total Base256 required size. - let size : Nat = (input.size() - zeroes - spaces) * 733 / 1000 + 1; + let size : Nat = (inputSize - pos) * 733 / 1000 + 1; let b256 : [var Nat16] = VarArray.repeat(0x00, size); + var length : Nat = 0; - label l loop { - switch (current) { - case (?0x20) break l; - case (null) break l; - case (?value) { - var carry : Nat16 = mapBase58[value.toNat()]; - assert (carry != 0xff); - - var i : Nat = 0; - var b256Pointer : Nat = size - 1; - label reverseIter while (carry != 0 or i < length) { - - carry +%= 58 * b256[b256Pointer]; - b256[b256Pointer] := (carry & 0xff); - carry >>= 8; - i += 1; - - if (b256Pointer == 0) break reverseIter; - b256Pointer -= 1; - }; - - assert (carry == 0); - length := i; - }; + while (pos < inputSize and input[pos] != 0x20) { + var carry : Nat16 = mapBase58[input[pos].toNat()]; + assert (carry != 0xff); + + var i : Nat = 0; + var b256Pointer : Nat = size - 1; + label reverseIter while (carry != 0 or i < length) { + carry +%= 58 * b256[b256Pointer]; + b256[b256Pointer] := (carry & 0xff); + carry >>= 8; + i += 1; + + if (b256Pointer == 0) break reverseIter; + b256Pointer -= 1; }; - current := inputIter.next(); + + assert (carry == 0); + length := i; + pos += 1; }; // Skip trailing spaces. - label l loop { - switch (current) { - case (?0x20) {}; - case (_) break l; - }; - current := inputIter.next(); + while (pos < inputSize and input[pos] == 0x20) { + pos += 1; }; // Check all input was consumed. - assert (current == null); + assert (pos == inputSize); // Skip leading zeroes in base256 result. var b256Pointer : Nat = size - length; From 2ef65f4c04b065e377d1d504b4ccde3c4ce70c64 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:34:30 +0000 Subject: [PATCH 10/34] chore(bench): update benchmark.md [skip ci] --- benchmark.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/benchmark.md b/benchmark.md index ed3a81a..ba64592 100644 --- a/benchmark.md +++ b/benchmark.md @@ -22,7 +22,7 @@ Garbage Collection: ${\color{gray}0\\%}$ | | len 0 | len 10 | len 32 | len 64 | len 128 | | :----- | ----: | -----: | ------: | ------: | --------: | | encode | 1_182 | 26_059 | 210_836 | 800_098 | 3_114_843 | -| decode | 1_316 | 26_644 | 213_296 | 805_615 | 3_127_486 | +| decode | 1_012 | 20_924 | 165_200 | 621_442 | 2_407_086 | **Heap** @@ -38,7 +38,7 @@ Garbage Collection: ${\color{gray}0\\%}$ | | len 0 | len 10 | len 32 | len 64 | len 128 | | :----- | ----: | -----: | -----: | -------: | -------: | | encode | 372 B | 508 B | 808 B | 1.22 KiB | 2.07 KiB | -| decode | 424 B | 500 B | 676 B | 932 B | 1.41 KiB | +| decode | 348 B | 424 B | 600 B | 856 B | 1.34 KiB | @@ -63,7 +63,7 @@ Garbage Collection: ${\color{gray}0\\%}$ | | len 0 | len 10 | len 32 | len 64 | len 128 | | :----- | -----: | -----: | ------: | ------: | --------: | | encode | 41_565 | 84_651 | 309_351 | 961_324 | 3_398_416 | -| decode | 42_174 | 83_950 | 306_560 | 955_606 | 3_388_120 | +| decode | 40_490 | 73_546 | 246_249 | 748_346 | 2_622_434 | **Heap** @@ -79,7 +79,7 @@ Garbage Collection: ${\color{gray}0\\%}$ | | len 0 | len 10 | len 32 | len 64 | len 128 | | :----- | -------: | -------: | -------: | -------: | -------: | | encode | 3.91 KiB | 4.16 KiB | 4.63 KiB | 5.3 KiB | 6.66 KiB | -| decode | 3.94 KiB | 4.05 KiB | 4.31 KiB | 4.69 KiB | 5.44 KiB | +| decode | 3.87 KiB | 3.98 KiB | 4.24 KiB | 4.61 KiB | 5.36 KiB | @@ -105,8 +105,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | :------------- | -----: | -----: | -----: | -----: | | encode bech32 | 11_982 | 15_543 | 25_974 | 34_330 | | encode bech32m | 12_014 | 15_575 | 26_006 | 34_362 | -| decode bech32 | 10_694 | 15_066 | 27_559 | 37_597 | -| decode bech32m | 10_768 | 15_164 | 27_633 | 37_659 | +| decode bech32 | 10_696 | 15_068 | 27_561 | 37_599 | +| decode bech32m | 10_770 | 15_166 | 27_635 | 37_661 | **Heap** @@ -191,8 +191,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | 2 utxos | 4 utxos | | :------ | --------: | --------: | -| build | 574_019 | 582_087 | -| sighash | 1_127_651 | 1_135_719 | +| build | 480_326 | 488_394 | +| sighash | 1_002_664 | 1_010_795 | **Heap** @@ -207,8 +207,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | 2 utxos | 4 utxos | | :------ | --------: | --------: | -| build | 14.39 KiB | 14.86 KiB | -| sighash | 30.39 KiB | 30.86 KiB | +| build | 14.17 KiB | 14.64 KiB | +| sighash | 30.09 KiB | 30.56 KiB | @@ -355,8 +355,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | bc v0/20 | bc v0/32 | bc v1/32 | tb v0/20 | tb v0/32 | tb v1/32 | | :----- | -------: | -------: | -------: | -------: | -------: | -------: | -| encode | 146_027 | 204_067 | 204_039 | 145_963 | 204_111 | 204_099 | -| decode | 70_224 | 99_696 | 99_687 | 70_192 | 99_736 | 99_711 | +| encode | 146_029 | 204_069 | 204_041 | 145_965 | 204_113 | 204_101 | +| decode | 70_226 | 99_698 | 99_689 | 70_194 | 99_738 | 99_713 | **Heap** From d691e37c899f70fb97c5ed750ee90a2b6cea47c5 Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Thu, 16 Apr 2026 15:45:06 +0200 Subject: [PATCH 11/34] initial AI version of Base58.decode with batching --- src/Base58.mo | 84 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 28 deletions(-) diff --git a/src/Base58.mo b/src/Base58.mo index 6f01f30..7e89c15 100755 --- a/src/Base58.mo +++ b/src/Base58.mo @@ -49,40 +49,73 @@ module { }; // Skip and count leading '1's. - let start = pos; - + var zeroes : Nat = 0; while (pos < inputSize and input[pos] == 0x31) { + zeroes += 1; pos += 1; }; - let zeroes : Nat = pos - start; - // Compute how many bytes are needed for the Base256 representation. We - // need log(58) / log(256) of one byte to represent a Base58 digit in - // Base256, which is approximately 733 / 1000. The input size is multiplied - // by this value and rounded up to get the total Base256 required size. - let size : Nat = (inputSize - pos) * 733 / 1000 + 1; + // Find end of base58 payload (before trailing spaces). + var endPos = pos; + while (endPos < inputSize and input[endPos] != 0x20) { + endPos += 1; + }; + let digitCount = endPos - pos; + + // Allocate base256 buffer: log(58)/log(256) ≈ 733/1000. + let size : Nat = digitCount * 733 / 1000 + 1; let b256 : [var Nat16] = VarArray.repeat(0x00, size); var length : Nat = 0; - while (pos < inputSize and input[pos] != 0x20) { + // Process leading remainder digits (digitCount % 4) one at a time. + let remainder = digitCount % 4; + var rem : Nat = 0; + while (rem < remainder) { var carry : Nat16 = mapBase58[input[pos].toNat()]; assert (carry != 0xff); var i : Nat = 0; - var b256Pointer : Nat = size - 1; - label reverseIter while (carry != 0 or i < length) { - carry +%= 58 * b256[b256Pointer]; - b256[b256Pointer] := (carry & 0xff); + var j : Nat = size - 1; + label inner while (carry != 0 or i < length) { + carry +%= 58 * b256[j]; + b256[j] := (carry & 0xff); carry >>= 8; i += 1; - - if (b256Pointer == 0) break reverseIter; - b256Pointer -= 1; + if (j == 0) break inner; + j -= 1; }; assert (carry == 0); length := i; pos += 1; + rem += 1; + }; + + // Process full batches of 4 digits: b256 = b256 * 58^4 + v. + // 58^4 = 11_316_496. Max carry = 58^4 * 256 = 2_897_022_976 < 2^32. + while (pos < endPos) { + let d0 : Nat32 = mapBase58[input[pos].toNat()].toNat32(); + let d1 : Nat32 = mapBase58[input[pos + 1].toNat()].toNat32(); + let d2 : Nat32 = mapBase58[input[pos + 2].toNat()].toNat32(); + let d3 : Nat32 = mapBase58[input[pos + 3].toNat()].toNat32(); + assert (d0 != 0xff and d1 != 0xff and d2 != 0xff and d3 != 0xff); + + var carry : Nat32 = ((d0 * 58 + d1) * 58 + d2) * 58 + d3; + + var i : Nat = 0; + var j : Nat = size - 1; + label inner while (carry != 0 or i < length) { + carry +%= 11_316_496 * b256[j].toNat32(); + b256[j] := (carry & 0xff).toNat16(); + carry >>= 8; + i += 1; + if (j == 0) break inner; + j -= 1; + }; + + assert (carry == 0); + length := i; + pos += 4; }; // Skip trailing spaces. @@ -94,23 +127,18 @@ module { assert (pos == inputSize); // Skip leading zeroes in base256 result. - var b256Pointer : Nat = size - length; - while (b256Pointer < b256.size() and b256[b256Pointer] == 0) { - b256Pointer += 1; + var start : Nat = size - length; + while (start < size and b256[start] == 0) { + start += 1; }; - let output = Array.tabulate( - zeroes + b256.size() - b256Pointer, + Array.tabulate( + zeroes + size - start, func(i) { - if (i < zeroes) { - 0x00; - } else { - b256[i + b256Pointer - zeroes].toNat8(); - }; + if (i < zeroes) 0x00 + else b256[i + start - zeroes].toNat8(); }, ); - - output; }; // Convert the given Base256 input to Base58. From ced49d0ca418a6897910295ed6ade871a3bd689c Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Thu, 16 Apr 2026 15:54:17 +0200 Subject: [PATCH 12/34] Improve Bse58.decode to batches of 8 characters --- src/Base58.mo | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/Base58.mo b/src/Base58.mo index 7e89c15..8cd1881 100755 --- a/src/Base58.mo +++ b/src/Base58.mo @@ -2,6 +2,7 @@ import Array "mo:core/Array"; import Blob "mo:core/Blob"; import Nat16 "mo:core/Nat16"; import Nat32 "mo:core/Nat32"; +import Nat64 "mo:core/Nat64"; import Nat8 "mo:core/Nat8"; import Runtime "mo:core/Runtime"; import Text "mo:core/Text"; @@ -60,15 +61,15 @@ module { while (endPos < inputSize and input[endPos] != 0x20) { endPos += 1; }; - let digitCount = endPos - pos; + let digitCount : Nat = endPos - pos; // Allocate base256 buffer: log(58)/log(256) ≈ 733/1000. let size : Nat = digitCount * 733 / 1000 + 1; let b256 : [var Nat16] = VarArray.repeat(0x00, size); var length : Nat = 0; - // Process leading remainder digits (digitCount % 4) one at a time. - let remainder = digitCount % 4; + // Process leading remainder digits (digitCount % 8) one at a time. + let remainder = digitCount % 8; var rem : Nat = 0; while (rem < remainder) { var carry : Nat16 = mapBase58[input[pos].toNat()]; @@ -91,21 +92,26 @@ module { rem += 1; }; - // Process full batches of 4 digits: b256 = b256 * 58^4 + v. - // 58^4 = 11_316_496. Max carry = 58^4 * 256 = 2_897_022_976 < 2^32. + // Process full batches of 8 digits: b256 = b256 * 58^8 + v. + // 58^8 = 128_063_081_718_016. Max carry < 2^55, fits in Nat64. while (pos < endPos) { - let d0 : Nat32 = mapBase58[input[pos].toNat()].toNat32(); - let d1 : Nat32 = mapBase58[input[pos + 1].toNat()].toNat32(); - let d2 : Nat32 = mapBase58[input[pos + 2].toNat()].toNat32(); - let d3 : Nat32 = mapBase58[input[pos + 3].toNat()].toNat32(); - assert (d0 != 0xff and d1 != 0xff and d2 != 0xff and d3 != 0xff); - - var carry : Nat32 = ((d0 * 58 + d1) * 58 + d2) * 58 + d3; + let d0 : Nat64 = mapBase58[input[pos].toNat()].toNat64(); + let d1 : Nat64 = mapBase58[input[pos + 1].toNat()].toNat64(); + let d2 : Nat64 = mapBase58[input[pos + 2].toNat()].toNat64(); + let d3 : Nat64 = mapBase58[input[pos + 3].toNat()].toNat64(); + let d4 : Nat64 = mapBase58[input[pos + 4].toNat()].toNat64(); + let d5 : Nat64 = mapBase58[input[pos + 5].toNat()].toNat64(); + let d6 : Nat64 = mapBase58[input[pos + 6].toNat()].toNat64(); + let d7 : Nat64 = mapBase58[input[pos + 7].toNat()].toNat64(); + assert (d0 != 0xff and d1 != 0xff and d2 != 0xff and d3 != 0xff + and d4 != 0xff and d5 != 0xff and d6 != 0xff and d7 != 0xff); + + var carry : Nat64 = (((((((d0 *% 58 +% d1) *% 58 +% d2) *% 58 +% d3) *% 58 +% d4) *% 58 +% d5) *% 58 +% d6) *% 58 +% d7); var i : Nat = 0; var j : Nat = size - 1; label inner while (carry != 0 or i < length) { - carry +%= 11_316_496 * b256[j].toNat32(); + carry +%= 128_063_081_718_016 *% b256[j].toNat64(); b256[j] := (carry & 0xff).toNat16(); carry >>= 8; i += 1; @@ -115,7 +121,7 @@ module { assert (carry == 0); length := i; - pos += 4; + pos += 8; }; // Skip trailing spaces. From 460da81bb40a564638cc853e68367f87feb88c2d Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Thu, 16 Apr 2026 20:15:29 +0200 Subject: [PATCH 13/34] Improve Bse58.encode to batches of 7 bytes --- src/Base58.mo | 76 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/src/Base58.mo b/src/Base58.mo index 8cd1881..688755b 100755 --- a/src/Base58.mo +++ b/src/Base58.mo @@ -19,7 +19,7 @@ module { ]; // prettier-ignore - private let mapBase58 : [Nat16] = [ + private let mapBase58 : [Nat] = [ 255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255, 255, 0, @@ -72,7 +72,7 @@ module { let remainder = digitCount % 8; var rem : Nat = 0; while (rem < remainder) { - var carry : Nat16 = mapBase58[input[pos].toNat()]; + var carry : Nat16 = Nat16.fromIntWrap(mapBase58[input[pos].toNat()]); assert (carry != 0xff); var i : Nat = 0; @@ -95,14 +95,14 @@ module { // Process full batches of 8 digits: b256 = b256 * 58^8 + v. // 58^8 = 128_063_081_718_016. Max carry < 2^55, fits in Nat64. while (pos < endPos) { - let d0 : Nat64 = mapBase58[input[pos].toNat()].toNat64(); - let d1 : Nat64 = mapBase58[input[pos + 1].toNat()].toNat64(); - let d2 : Nat64 = mapBase58[input[pos + 2].toNat()].toNat64(); - let d3 : Nat64 = mapBase58[input[pos + 3].toNat()].toNat64(); - let d4 : Nat64 = mapBase58[input[pos + 4].toNat()].toNat64(); - let d5 : Nat64 = mapBase58[input[pos + 5].toNat()].toNat64(); - let d6 : Nat64 = mapBase58[input[pos + 6].toNat()].toNat64(); - let d7 : Nat64 = mapBase58[input[pos + 7].toNat()].toNat64(); + let d0 = Nat64.fromIntWrap(mapBase58[input[pos].toNat()]); + let d1 = Nat64.fromIntWrap(mapBase58[input[pos + 1].toNat()]); + let d2 = Nat64.fromIntWrap(mapBase58[input[pos + 2].toNat()]); + let d3 = Nat64.fromIntWrap(mapBase58[input[pos + 3].toNat()]); + let d4 = Nat64.fromIntWrap(mapBase58[input[pos + 4].toNat()]); + let d5 = Nat64.fromIntWrap(mapBase58[input[pos + 5].toNat()]); + let d6 = Nat64.fromIntWrap(mapBase58[input[pos + 6].toNat()]); + let d7 = Nat64.fromIntWrap(mapBase58[input[pos + 7].toNat()]); assert (d0 != 0xff and d1 != 0xff and d2 != 0xff and d3 != 0xff and d4 != 0xff and d5 != 0xff and d6 != 0xff and d7 != 0xff); @@ -111,8 +111,8 @@ module { var i : Nat = 0; var j : Nat = size - 1; label inner while (carry != 0 or i < length) { - carry +%= 128_063_081_718_016 *% b256[j].toNat64(); - b256[j] := (carry & 0xff).toNat16(); + carry +%= 128_063_081_718_016 *% b256[j].toNat32().toNat64(); + b256[j] := (carry & 0xff).toNat32().toNat16(); carry >>= 8; i += 1; if (j == 0) break inner; @@ -162,26 +162,56 @@ module { // Allocate enough space in big-endian base58 representation: // log(256) / log(58), rounded up. let size : Nat = (input.size() - inputPointer) * 138 / 100 + 1; - let b58 : [var Nat8] = VarArray.repeat(0, size); + let b58 : [var Nat16] = VarArray.repeat(0, size); + let inputSize = input.size(); + let remainingBytes : Nat = inputSize - inputPointer; - while (inputPointer < input.size()) { - var carry : Nat32 = input[inputPointer].toNat16().toNat32(); + // Process leading remainder bytes (remainingBytes % 7) one at a time. + let remainder = remainingBytes % 7; + var rem : Nat = 0; + while (rem < remainder) { + var carry : Nat16 = input[inputPointer].toNat16(); var i : Nat = 0; - // Apply "b58 = b58 * 256 + ch". - var b58Pointer : Nat = b58.size() - 1; - label reverseIter while (carry != 0 or i < length) { - carry += 256 * b58[b58Pointer].toNat16().toNat32(); - b58[b58Pointer] := Nat8.fromNat((carry % 58).toNat()); + var b58Pointer : Nat = size - 1; + label inner while (carry != 0 or i < length) { + carry +%= 256 *% b58[b58Pointer]; + b58[b58Pointer] := carry % 58; carry /= 58; i += 1; - if (b58Pointer == 0) { - break reverseIter; - }; + if (b58Pointer == 0) break inner; b58Pointer -= 1; }; assert (carry == 0); length := i; inputPointer += 1; + rem += 1; + }; + + // Process full batches of 7 bytes: b58 = b58 * 256^7 + v. + // 256^7 = 72_057_594_037_927_936. Max carry < 2^62, fits in Nat64. + while (inputPointer < inputSize) { + var carry : Nat64 = + Nat64.fromIntWrap(input[inputPointer].toNat()) << 48 + | Nat64.fromIntWrap(input[inputPointer + 1].toNat()) << 40 + | Nat64.fromIntWrap(input[inputPointer + 2].toNat()) << 32 + | Nat64.fromIntWrap(input[inputPointer + 3].toNat()) << 24 + | Nat64.fromIntWrap(input[inputPointer + 4].toNat()) << 16 + | Nat64.fromIntWrap(input[inputPointer + 5].toNat()) << 8 + | Nat64.fromIntWrap(input[inputPointer + 6].toNat()); + + var i : Nat = 0; + var b58Pointer : Nat = size - 1; + label inner while (carry != 0 or i < length) { + carry +%= 72_057_594_037_927_936 *% b58[b58Pointer].toNat32().toNat64(); + b58[b58Pointer] := (carry % 58).toNat32().toNat16(); + carry /= 58; + i += 1; + if (b58Pointer == 0) break inner; + b58Pointer -= 1; + }; + assert (carry == 0); + length := i; + inputPointer += 7; }; // Skip leading zeroes in base58 result. From d7455300ffb5accb9a449475b51fceb739f55b95 Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Thu, 16 Apr 2026 20:57:58 +0200 Subject: [PATCH 14/34] Code improvements --- src/Base58.mo | 62 +++++++++++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/src/Base58.mo b/src/Base58.mo index 688755b..3488bff 100755 --- a/src/Base58.mo +++ b/src/Base58.mo @@ -40,7 +40,7 @@ module { // Convert the given Base58 input to Base256. public func decode(input_ : Text) : [Nat8] { - let input = Text.encodeUtf8(input_); + let input : Blob = Text.encodeUtf8(input_); let inputSize = input.size(); var pos : Nat = 0; @@ -50,16 +50,16 @@ module { }; // Skip and count leading '1's. - var zeroes : Nat = 0; + let startPos = pos; while (pos < inputSize and input[pos] == 0x31) { - zeroes += 1; pos += 1; }; + let zeroes : Nat = pos - startPos; // Find end of base58 payload (before trailing spaces). - var endPos = pos; - while (endPos < inputSize and input[endPos] != 0x20) { - endPos += 1; + var endPos = inputSize; + while (endPos > pos and input[endPos - 1] == 0x20) { + endPos -= 1; }; let digitCount : Nat = endPos - pos; @@ -103,8 +103,9 @@ module { let d5 = Nat64.fromIntWrap(mapBase58[input[pos + 5].toNat()]); let d6 = Nat64.fromIntWrap(mapBase58[input[pos + 6].toNat()]); let d7 = Nat64.fromIntWrap(mapBase58[input[pos + 7].toNat()]); - assert (d0 != 0xff and d1 != 0xff and d2 != 0xff and d3 != 0xff - and d4 != 0xff and d5 != 0xff and d6 != 0xff and d7 != 0xff); + assert ( + d0 != 0xff and d1 != 0xff and d2 != 0xff and d3 != 0xff and d4 != 0xff and d5 != 0xff and d6 != 0xff and d7 != 0xff + ); var carry : Nat64 = (((((((d0 *% 58 +% d1) *% 58 +% d2) *% 58 +% d3) *% 58 +% d4) *% 58 +% d5) *% 58 +% d6) *% 58 +% d7); @@ -141,36 +142,34 @@ module { Array.tabulate( zeroes + size - start, func(i) { - if (i < zeroes) 0x00 - else b256[i + start - zeroes].toNat8(); + if (i < zeroes) 0x00 else b256[i + start - zeroes].toNat8(); }, ); }; // Convert the given Base256 input to Base58. public func encode(input : [Nat8]) : Text { - var zeroes : Nat = 0; + let inputSize = input.size(); var length : Nat = 0; - var inputPointer : Nat = 0; + var pos : Nat = 0; // Skip & count leading zeroes. - while (zeroes < input.size() and input[inputPointer] == 0) { - zeroes += 1; - inputPointer += 1; + while (pos < inputSize and input[pos] == 0) { + pos += 1; }; + let zeroes : Nat = pos; // Allocate enough space in big-endian base58 representation: // log(256) / log(58), rounded up. - let size : Nat = (input.size() - inputPointer) * 138 / 100 + 1; + let bytesCount : Nat = inputSize - pos; + let size : Nat = bytesCount * 138 / 100 + 1; let b58 : [var Nat16] = VarArray.repeat(0, size); - let inputSize = input.size(); - let remainingBytes : Nat = inputSize - inputPointer; // Process leading remainder bytes (remainingBytes % 7) one at a time. - let remainder = remainingBytes % 7; + let remainder = bytesCount % 7; var rem : Nat = 0; while (rem < remainder) { - var carry : Nat16 = input[inputPointer].toNat16(); + var carry : Nat16 = input[pos].toNat16(); var i : Nat = 0; var b58Pointer : Nat = size - 1; label inner while (carry != 0 or i < length) { @@ -183,21 +182,14 @@ module { }; assert (carry == 0); length := i; - inputPointer += 1; + pos += 1; rem += 1; }; // Process full batches of 7 bytes: b58 = b58 * 256^7 + v. // 256^7 = 72_057_594_037_927_936. Max carry < 2^62, fits in Nat64. - while (inputPointer < inputSize) { - var carry : Nat64 = - Nat64.fromIntWrap(input[inputPointer].toNat()) << 48 - | Nat64.fromIntWrap(input[inputPointer + 1].toNat()) << 40 - | Nat64.fromIntWrap(input[inputPointer + 2].toNat()) << 32 - | Nat64.fromIntWrap(input[inputPointer + 3].toNat()) << 24 - | Nat64.fromIntWrap(input[inputPointer + 4].toNat()) << 16 - | Nat64.fromIntWrap(input[inputPointer + 5].toNat()) << 8 - | Nat64.fromIntWrap(input[inputPointer + 6].toNat()); + while (pos < inputSize) { + var carry : Nat64 = Nat64.fromIntWrap(input[pos].toNat()) << 48 | Nat64.fromIntWrap(input[pos + 1].toNat()) << 40 | Nat64.fromIntWrap(input[pos + 2].toNat()) << 32 | Nat64.fromIntWrap(input[pos + 3].toNat()) << 24 | Nat64.fromIntWrap(input[pos + 4].toNat()) << 16 | Nat64.fromIntWrap(input[pos + 5].toNat()) << 8 | Nat64.fromIntWrap(input[pos + 6].toNat()); var i : Nat = 0; var b58Pointer : Nat = size - 1; @@ -211,20 +203,22 @@ module { }; assert (carry == 0); length := i; - inputPointer += 7; + pos += 7; }; // Skip leading zeroes in base58 result. var b58Pointer : Nat = size - length; - while (b58Pointer < b58.size() and b58[b58Pointer] == 0) { b58Pointer += 1 }; + while (b58Pointer < size and b58[b58Pointer] == 0) { + b58Pointer += 1; + }; let outputBytes = Array.tabulate( - zeroes + b58.size() - b58Pointer, + zeroes + size - b58Pointer, func(i) { if (i < zeroes) { 0x31 : Nat8; } else { - base58Alphabet[b58[i + b58Pointer - zeroes].toNat()]; + base58Alphabet[b58[b58Pointer + i - zeroes].toNat()]; }; }, ); From 8cf5d5b0db3ece15cda5f0cff47ed7044db99d9d Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Thu, 16 Apr 2026 21:35:33 +0200 Subject: [PATCH 15/34] Small code improvements --- src/Bech32.mo | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Bech32.mo b/src/Bech32.mo index 46357b5..57800cf 100644 --- a/src/Bech32.mo +++ b/src/Bech32.mo @@ -199,9 +199,7 @@ module { Array.tabulate( 6, func(i) { - Nat8.fromIntWrap( - ((mod >> (5 * (5 - Nat32.fromIntWrap(i)))) & 31).toNat() - ); + ((mod >> (5 * (5 - Nat32.fromIntWrap(i)))) & 31).toNat16().toNat8(); }, ); }; @@ -229,7 +227,7 @@ module { var c : Nat32 = 1; for (value in values.values()) { - let c0 : Nat8 = Nat8.fromIntWrap((c >> 25).toNat()); + let c0 : Nat8 = (c >> 25).toNat16().toNat8(); c := ((c & 0x1ffffff) << 5) ^ value.toNat16().toNat32(); // Conditionally add in coefficients of the generator polynomial. From e580effb09eb8fe200bad9ffdd7c2bd857739d9d Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Thu, 16 Apr 2026 21:35:47 +0200 Subject: [PATCH 16/34] Optimize Segwit.mo by avoiding List and iters --- src/Segwit.mo | 119 +++++++++++++++++++++----------------------------- 1 file changed, 50 insertions(+), 69 deletions(-) diff --git a/src/Segwit.mo b/src/Segwit.mo index 3796f1e..ab9de59 100644 --- a/src/Segwit.mo +++ b/src/Segwit.mo @@ -1,10 +1,10 @@ -import List "mo:core/List"; +import Array "mo:core/Array"; import Nat "mo:core/Nat"; import Nat16 "mo:core/Nat16"; import Nat32 "mo:core/Nat32"; import Nat8 "mo:core/Nat8"; -import Runtime "mo:core/Runtime"; -import { type Result; type Iter } "mo:core/Types"; +import { type Result } "mo:core/Types"; +import VarArray "mo:core/VarArray"; import Bech32 "Bech32"; @@ -18,14 +18,9 @@ module { // Convert a Witness Program to a SegWit Address. public func encode(hrp : Text, { version; program } : WitnessProgram) : Result { - let bech32Input = List.empty(); - bech32Input.add(version); - - switch (convertBits(program.values(), bech32Input, 8, 5, true)) { - case (#err(msg)) { - return #err(msg); - }; - case _ {}; + let converted = switch (convertBits(program, 0, 8, 5, true)) { + case (#err(msg)) return #err(msg); + case (#ok(c)) c; }; let encoding : Bech32.Encoding = if (version > 0) { @@ -36,7 +31,7 @@ module { let bech32Result : Text = Bech32.encode( hrp, - bech32Input.toArray(), + [[version] : [Nat8], converted].flatten(), encoding, ); @@ -68,69 +63,60 @@ module { return #err("Invalid data length."); }; - // Split into version and program. - let dataIter : Iter = data.values(); - let version : Nat8 = switch (dataIter.next()) { - case (?val) { - val; - }; - case _ { - Runtime.trap("unreachable"); - }; + let version : Nat8 = data[0]; + + let convertedData = switch (convertBits(data, 1, 5, 8, false)) { + case (#ok(d)) d; + case _ return #err("Convert bits failed."); }; - let convertedData = List.empty(); - switch (convertBits(dataIter, convertedData, 5, 8, false)) { - case (#ok) { - let convertedDataSize : Nat = convertedData.size(); - - if (convertedDataSize < 2 or convertedDataSize > 40) { - return #err("Wrong output size."); - }; - - if (data[0] > 16) { - return #err("Invalid witness version."); - }; - - if ( - data[0] == 0 and convertedDataSize != 20 and convertedDataSize != 32 - ) { - return #err("Program size does not match witness version."); - }; - - if ( - data[0] == 0 and encoding != #BECH32 or - data[0] != 0 and encoding != #BECH32M - ) { - return #err("Encoding does not match witness version."); - }; - - return #ok(decodedHrp, { version; program = convertedData.toArray() }); - }; - case _ { - return #err("Convert bits failed."); - }; + let convertedDataSize : Nat = convertedData.size(); + + if (convertedDataSize < 2 or convertedDataSize > 40) { + return #err("Wrong output size."); + }; + + if (data[0] > 16) { + return #err("Invalid witness version."); + }; + + if ( + data[0] == 0 and convertedDataSize != 20 and convertedDataSize != 32 + ) { + return #err("Program size does not match witness version."); + }; + + if ( + data[0] == 0 and encoding != #BECH32 or + data[0] != 0 and encoding != #BECH32M + ) { + return #err("Encoding does not match witness version."); }; + + #ok(decodedHrp, { version; program = convertedData }); }; // Convert between two bases that are power of 2. func convertBits( - data : Iter, - output : List.List, + data : [Nat8], + start : Nat, from : Nat32, to : Nat32, pad : Bool, - ) : Result<(), Text> { + ) : Result<[Nat8], Text> { var acc : Nat32 = 0; var bits : Nat32 = 0; let maxv : Nat32 = (1 << to) - 1; + let output = VarArray.repeat(0, data.size() * from.toNat() / to.toNat() + 1); + var outputLen : Nat = 0; - for (value in data) { - let v : Nat32 = value.toNat16().toNat32(); + var pos = start; + while (pos < data.size()) { + let v : Nat32 = data[pos].toNat16().toNat32(); if ((v >> from) != 0) { - return #err("Invalid input value: " # value.toNat().toText()); + return #err("Invalid input value: " # data[pos].toNat().toText()); }; acc := (acc << from) | v; @@ -138,26 +124,21 @@ module { while (bits >= to) { bits -= to; - output.add( - Nat8.fromIntWrap( - ((acc >> bits) & maxv).toNat() - ) - ); + output[outputLen] := ((acc >> bits) & maxv).toNat16().toNat8(); + outputLen += 1; }; + pos += 1; }; if (pad) { if (bits > 0) { - output.add( - Nat8.fromIntWrap( - ((acc << (to - bits)) & maxv).toNat() - ) - ); + output[outputLen] := ((acc << (to - bits)) & maxv).toNat16().toNat8(); + outputLen += 1; }; } else if (bits >= from or ((acc << (to - bits)) & maxv) != 0) { return #err("Invalid Padding"); }; - return #ok; + #ok(Array.tabulate(outputLen, func(i) = output[i])); }; }; From 220b518b569b7a103e36af132963fd1c93d74210 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 20:13:52 +0000 Subject: [PATCH 17/34] chore(bench): update benchmark.md [skip ci] --- benchmark.md | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/benchmark.md b/benchmark.md index ed3a81a..f2dbbbf 100644 --- a/benchmark.md +++ b/benchmark.md @@ -19,10 +19,10 @@ Garbage Collection: ${\color{gray}0\\%}$ **Instructions** -| | len 0 | len 10 | len 32 | len 64 | len 128 | -| :----- | ----: | -----: | ------: | ------: | --------: | -| encode | 1_182 | 26_059 | 210_836 | 800_098 | 3_114_843 | -| decode | 1_316 | 26_644 | 213_296 | 805_615 | 3_127_486 | +| | len 0 | len 10 | len 32 | len 64 | len 128 | +| :----- | ----: | -----: | -----: | ------: | ------: | +| encode | 1_193 | 10_490 | 44_319 | 127_586 | 429_639 | +| decode | 1_092 | 10_192 | 34_994 | 106_525 | 347_882 | **Heap** @@ -37,8 +37,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | len 0 | len 10 | len 32 | len 64 | len 128 | | :----- | ----: | -----: | -----: | -------: | -------: | -| encode | 372 B | 508 B | 808 B | 1.22 KiB | 2.07 KiB | -| decode | 424 B | 500 B | 676 B | 932 B | 1.41 KiB | +| encode | 364 B | 500 B | 800 B | 1.21 KiB | 2.07 KiB | +| decode | 348 B | 424 B | 600 B | 856 B | 1.34 KiB | @@ -60,10 +60,10 @@ Garbage Collection: ${\color{gray}0\\%}$ **Instructions** -| | len 0 | len 10 | len 32 | len 64 | len 128 | -| :----- | -----: | -----: | ------: | ------: | --------: | -| encode | 41_565 | 84_651 | 309_351 | 961_324 | 3_398_416 | -| decode | 42_174 | 83_950 | 306_560 | 955_606 | 3_388_120 | +| | len 0 | len 10 | len 32 | len 64 | len 128 | +| :----- | -----: | -----: | -----: | ------: | ------: | +| encode | 40_619 | 52_094 | 96_434 | 205_225 | 546_308 | +| decode | 40_702 | 48_521 | 80_562 | 164_580 | 430_823 | **Heap** @@ -78,8 +78,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | len 0 | len 10 | len 32 | len 64 | len 128 | | :----- | -------: | -------: | -------: | -------: | -------: | -| encode | 3.91 KiB | 4.16 KiB | 4.63 KiB | 5.3 KiB | 6.66 KiB | -| decode | 3.94 KiB | 4.05 KiB | 4.31 KiB | 4.69 KiB | 5.44 KiB | +| encode | 3.91 KiB | 4.16 KiB | 4.63 KiB | 5.29 KiB | 6.66 KiB | +| decode | 3.87 KiB | 3.98 KiB | 4.24 KiB | 4.61 KiB | 5.36 KiB | @@ -103,10 +103,10 @@ Garbage Collection: ${\color{gray}0\\%}$ | | len 0 | len 5 | len 20 | len 32 | | :------------- | -----: | -----: | -----: | -----: | -| encode bech32 | 11_982 | 15_543 | 25_974 | 34_330 | -| encode bech32m | 12_014 | 15_575 | 26_006 | 34_362 | -| decode bech32 | 10_694 | 15_066 | 27_559 | 37_597 | -| decode bech32m | 10_768 | 15_164 | 27_633 | 37_659 | +| encode bech32 | 11_965 | 15_521 | 25_937 | 34_281 | +| encode bech32m | 11_997 | 15_553 | 25_969 | 34_313 | +| decode bech32 | 10_685 | 15_052 | 27_530 | 37_556 | +| decode bech32m | 10_759 | 15_150 | 27_604 | 37_618 | **Heap** @@ -189,10 +189,10 @@ Garbage Collection: ${\color{gray}0\\%}$ **Instructions** -| | 2 utxos | 4 utxos | -| :------ | --------: | --------: | -| build | 574_019 | 582_087 | -| sighash | 1_127_651 | 1_135_719 | +| | 2 utxos | 4 utxos | +| :------ | ------: | ------: | +| build | 230_594 | 238_662 | +| sighash | 669_664 | 677_795 | **Heap** @@ -207,8 +207,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | 2 utxos | 4 utxos | | :------ | --------: | --------: | -| build | 14.39 KiB | 14.86 KiB | -| sighash | 30.39 KiB | 30.86 KiB | +| build | 14.17 KiB | 14.64 KiB | +| sighash | 30.09 KiB | 30.56 KiB | @@ -355,8 +355,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | bc v0/20 | bc v0/32 | bc v1/32 | tb v0/20 | tb v0/32 | tb v1/32 | | :----- | -------: | -------: | -------: | -------: | -------: | -------: | -| encode | 146_027 | 204_067 | 204_039 | 145_963 | 204_111 | 204_099 | -| decode | 70_224 | 99_696 | 99_687 | 70_192 | 99_736 | 99_711 | +| encode | 103_139 | 150_473 | 150_445 | 103_075 | 150_517 | 150_505 | +| decode | 49_388 | 72_072 | 72_063 | 49_356 | 72_112 | 72_087 | **Heap** @@ -371,8 +371,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | bc v0/20 | bc v0/32 | bc v1/32 | tb v0/20 | tb v0/32 | tb v1/32 | | :----- | -------: | -------: | -------: | -------: | -------: | -------: | -| encode | 4.06 KiB | 5 KiB | 5 KiB | 4.06 KiB | 5 KiB | 5 KiB | -| decode | 2.18 KiB | 2.68 KiB | 2.68 KiB | 2.18 KiB | 2.68 KiB | 2.68 KiB | +| encode | 3.21 KiB | 4.21 KiB | 4.21 KiB | 3.21 KiB | 4.21 KiB | 4.21 KiB | +| decode | 1.72 KiB | 2.21 KiB | 2.21 KiB | 1.72 KiB | 2.21 KiB | 2.21 KiB | From 93d91553104fb31aae2e360309bb385fab7a89aa Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Thu, 16 Apr 2026 22:20:41 +0200 Subject: [PATCH 18/34] Optimize with batching (#7) * Improve Base58.decode to batches of 8 characters * Improve Bse58.encode to batches of 7 bytes * Optimize Segwit.mo by avoiding List and iters --- benchmark.md | 44 +++++++------- src/Base58.mo | 162 ++++++++++++++++++++++++++++++++++---------------- src/Bech32.mo | 6 +- src/Segwit.mo | 119 ++++++++++++++++-------------------- 4 files changed, 184 insertions(+), 147 deletions(-) diff --git a/benchmark.md b/benchmark.md index ba64592..f2dbbbf 100644 --- a/benchmark.md +++ b/benchmark.md @@ -19,10 +19,10 @@ Garbage Collection: ${\color{gray}0\\%}$ **Instructions** -| | len 0 | len 10 | len 32 | len 64 | len 128 | -| :----- | ----: | -----: | ------: | ------: | --------: | -| encode | 1_182 | 26_059 | 210_836 | 800_098 | 3_114_843 | -| decode | 1_012 | 20_924 | 165_200 | 621_442 | 2_407_086 | +| | len 0 | len 10 | len 32 | len 64 | len 128 | +| :----- | ----: | -----: | -----: | ------: | ------: | +| encode | 1_193 | 10_490 | 44_319 | 127_586 | 429_639 | +| decode | 1_092 | 10_192 | 34_994 | 106_525 | 347_882 | **Heap** @@ -37,7 +37,7 @@ Garbage Collection: ${\color{gray}0\\%}$ | | len 0 | len 10 | len 32 | len 64 | len 128 | | :----- | ----: | -----: | -----: | -------: | -------: | -| encode | 372 B | 508 B | 808 B | 1.22 KiB | 2.07 KiB | +| encode | 364 B | 500 B | 800 B | 1.21 KiB | 2.07 KiB | | decode | 348 B | 424 B | 600 B | 856 B | 1.34 KiB | @@ -60,10 +60,10 @@ Garbage Collection: ${\color{gray}0\\%}$ **Instructions** -| | len 0 | len 10 | len 32 | len 64 | len 128 | -| :----- | -----: | -----: | ------: | ------: | --------: | -| encode | 41_565 | 84_651 | 309_351 | 961_324 | 3_398_416 | -| decode | 40_490 | 73_546 | 246_249 | 748_346 | 2_622_434 | +| | len 0 | len 10 | len 32 | len 64 | len 128 | +| :----- | -----: | -----: | -----: | ------: | ------: | +| encode | 40_619 | 52_094 | 96_434 | 205_225 | 546_308 | +| decode | 40_702 | 48_521 | 80_562 | 164_580 | 430_823 | **Heap** @@ -78,7 +78,7 @@ Garbage Collection: ${\color{gray}0\\%}$ | | len 0 | len 10 | len 32 | len 64 | len 128 | | :----- | -------: | -------: | -------: | -------: | -------: | -| encode | 3.91 KiB | 4.16 KiB | 4.63 KiB | 5.3 KiB | 6.66 KiB | +| encode | 3.91 KiB | 4.16 KiB | 4.63 KiB | 5.29 KiB | 6.66 KiB | | decode | 3.87 KiB | 3.98 KiB | 4.24 KiB | 4.61 KiB | 5.36 KiB | @@ -103,10 +103,10 @@ Garbage Collection: ${\color{gray}0\\%}$ | | len 0 | len 5 | len 20 | len 32 | | :------------- | -----: | -----: | -----: | -----: | -| encode bech32 | 11_982 | 15_543 | 25_974 | 34_330 | -| encode bech32m | 12_014 | 15_575 | 26_006 | 34_362 | -| decode bech32 | 10_696 | 15_068 | 27_561 | 37_599 | -| decode bech32m | 10_770 | 15_166 | 27_635 | 37_661 | +| encode bech32 | 11_965 | 15_521 | 25_937 | 34_281 | +| encode bech32m | 11_997 | 15_553 | 25_969 | 34_313 | +| decode bech32 | 10_685 | 15_052 | 27_530 | 37_556 | +| decode bech32m | 10_759 | 15_150 | 27_604 | 37_618 | **Heap** @@ -189,10 +189,10 @@ Garbage Collection: ${\color{gray}0\\%}$ **Instructions** -| | 2 utxos | 4 utxos | -| :------ | --------: | --------: | -| build | 480_326 | 488_394 | -| sighash | 1_002_664 | 1_010_795 | +| | 2 utxos | 4 utxos | +| :------ | ------: | ------: | +| build | 230_594 | 238_662 | +| sighash | 669_664 | 677_795 | **Heap** @@ -355,8 +355,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | bc v0/20 | bc v0/32 | bc v1/32 | tb v0/20 | tb v0/32 | tb v1/32 | | :----- | -------: | -------: | -------: | -------: | -------: | -------: | -| encode | 146_029 | 204_069 | 204_041 | 145_965 | 204_113 | 204_101 | -| decode | 70_226 | 99_698 | 99_689 | 70_194 | 99_738 | 99_713 | +| encode | 103_139 | 150_473 | 150_445 | 103_075 | 150_517 | 150_505 | +| decode | 49_388 | 72_072 | 72_063 | 49_356 | 72_112 | 72_087 | **Heap** @@ -371,8 +371,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | bc v0/20 | bc v0/32 | bc v1/32 | tb v0/20 | tb v0/32 | tb v1/32 | | :----- | -------: | -------: | -------: | -------: | -------: | -------: | -| encode | 4.06 KiB | 5 KiB | 5 KiB | 4.06 KiB | 5 KiB | 5 KiB | -| decode | 2.18 KiB | 2.68 KiB | 2.68 KiB | 2.18 KiB | 2.68 KiB | 2.68 KiB | +| encode | 3.21 KiB | 4.21 KiB | 4.21 KiB | 3.21 KiB | 4.21 KiB | 4.21 KiB | +| decode | 1.72 KiB | 2.21 KiB | 2.21 KiB | 1.72 KiB | 2.21 KiB | 2.21 KiB | diff --git a/src/Base58.mo b/src/Base58.mo index 6f01f30..3488bff 100755 --- a/src/Base58.mo +++ b/src/Base58.mo @@ -2,6 +2,7 @@ import Array "mo:core/Array"; import Blob "mo:core/Blob"; import Nat16 "mo:core/Nat16"; import Nat32 "mo:core/Nat32"; +import Nat64 "mo:core/Nat64"; import Nat8 "mo:core/Nat8"; import Runtime "mo:core/Runtime"; import Text "mo:core/Text"; @@ -18,7 +19,7 @@ module { ]; // prettier-ignore - private let mapBase58 : [Nat16] = [ + private let mapBase58 : [Nat] = [ 255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255, 255, 0, @@ -39,7 +40,7 @@ module { // Convert the given Base58 input to Base256. public func decode(input_ : Text) : [Nat8] { - let input = Text.encodeUtf8(input_); + let input : Blob = Text.encodeUtf8(input_); let inputSize = input.size(); var pos : Nat = 0; @@ -49,40 +50,79 @@ module { }; // Skip and count leading '1's. - let start = pos; - + let startPos = pos; while (pos < inputSize and input[pos] == 0x31) { pos += 1; }; - let zeroes : Nat = pos - start; + let zeroes : Nat = pos - startPos; + + // Find end of base58 payload (before trailing spaces). + var endPos = inputSize; + while (endPos > pos and input[endPos - 1] == 0x20) { + endPos -= 1; + }; + let digitCount : Nat = endPos - pos; - // Compute how many bytes are needed for the Base256 representation. We - // need log(58) / log(256) of one byte to represent a Base58 digit in - // Base256, which is approximately 733 / 1000. The input size is multiplied - // by this value and rounded up to get the total Base256 required size. - let size : Nat = (inputSize - pos) * 733 / 1000 + 1; + // Allocate base256 buffer: log(58)/log(256) ≈ 733/1000. + let size : Nat = digitCount * 733 / 1000 + 1; let b256 : [var Nat16] = VarArray.repeat(0x00, size); var length : Nat = 0; - while (pos < inputSize and input[pos] != 0x20) { - var carry : Nat16 = mapBase58[input[pos].toNat()]; + // Process leading remainder digits (digitCount % 8) one at a time. + let remainder = digitCount % 8; + var rem : Nat = 0; + while (rem < remainder) { + var carry : Nat16 = Nat16.fromIntWrap(mapBase58[input[pos].toNat()]); assert (carry != 0xff); var i : Nat = 0; - var b256Pointer : Nat = size - 1; - label reverseIter while (carry != 0 or i < length) { - carry +%= 58 * b256[b256Pointer]; - b256[b256Pointer] := (carry & 0xff); + var j : Nat = size - 1; + label inner while (carry != 0 or i < length) { + carry +%= 58 * b256[j]; + b256[j] := (carry & 0xff); carry >>= 8; i += 1; - - if (b256Pointer == 0) break reverseIter; - b256Pointer -= 1; + if (j == 0) break inner; + j -= 1; }; assert (carry == 0); length := i; pos += 1; + rem += 1; + }; + + // Process full batches of 8 digits: b256 = b256 * 58^8 + v. + // 58^8 = 128_063_081_718_016. Max carry < 2^55, fits in Nat64. + while (pos < endPos) { + let d0 = Nat64.fromIntWrap(mapBase58[input[pos].toNat()]); + let d1 = Nat64.fromIntWrap(mapBase58[input[pos + 1].toNat()]); + let d2 = Nat64.fromIntWrap(mapBase58[input[pos + 2].toNat()]); + let d3 = Nat64.fromIntWrap(mapBase58[input[pos + 3].toNat()]); + let d4 = Nat64.fromIntWrap(mapBase58[input[pos + 4].toNat()]); + let d5 = Nat64.fromIntWrap(mapBase58[input[pos + 5].toNat()]); + let d6 = Nat64.fromIntWrap(mapBase58[input[pos + 6].toNat()]); + let d7 = Nat64.fromIntWrap(mapBase58[input[pos + 7].toNat()]); + assert ( + d0 != 0xff and d1 != 0xff and d2 != 0xff and d3 != 0xff and d4 != 0xff and d5 != 0xff and d6 != 0xff and d7 != 0xff + ); + + var carry : Nat64 = (((((((d0 *% 58 +% d1) *% 58 +% d2) *% 58 +% d3) *% 58 +% d4) *% 58 +% d5) *% 58 +% d6) *% 58 +% d7); + + var i : Nat = 0; + var j : Nat = size - 1; + label inner while (carry != 0 or i < length) { + carry +%= 128_063_081_718_016 *% b256[j].toNat32().toNat64(); + b256[j] := (carry & 0xff).toNat32().toNat16(); + carry >>= 8; + i += 1; + if (j == 0) break inner; + j -= 1; + }; + + assert (carry == 0); + length := i; + pos += 8; }; // Skip trailing spaces. @@ -94,73 +134,91 @@ module { assert (pos == inputSize); // Skip leading zeroes in base256 result. - var b256Pointer : Nat = size - length; - while (b256Pointer < b256.size() and b256[b256Pointer] == 0) { - b256Pointer += 1; + var start : Nat = size - length; + while (start < size and b256[start] == 0) { + start += 1; }; - let output = Array.tabulate( - zeroes + b256.size() - b256Pointer, + Array.tabulate( + zeroes + size - start, func(i) { - if (i < zeroes) { - 0x00; - } else { - b256[i + b256Pointer - zeroes].toNat8(); - }; + if (i < zeroes) 0x00 else b256[i + start - zeroes].toNat8(); }, ); - - output; }; // Convert the given Base256 input to Base58. public func encode(input : [Nat8]) : Text { - var zeroes : Nat = 0; + let inputSize = input.size(); var length : Nat = 0; - var inputPointer : Nat = 0; + var pos : Nat = 0; // Skip & count leading zeroes. - while (zeroes < input.size() and input[inputPointer] == 0) { - zeroes += 1; - inputPointer += 1; + while (pos < inputSize and input[pos] == 0) { + pos += 1; }; + let zeroes : Nat = pos; // Allocate enough space in big-endian base58 representation: // log(256) / log(58), rounded up. - let size : Nat = (input.size() - inputPointer) * 138 / 100 + 1; - let b58 : [var Nat8] = VarArray.repeat(0, size); + let bytesCount : Nat = inputSize - pos; + let size : Nat = bytesCount * 138 / 100 + 1; + let b58 : [var Nat16] = VarArray.repeat(0, size); + + // Process leading remainder bytes (remainingBytes % 7) one at a time. + let remainder = bytesCount % 7; + var rem : Nat = 0; + while (rem < remainder) { + var carry : Nat16 = input[pos].toNat16(); + var i : Nat = 0; + var b58Pointer : Nat = size - 1; + label inner while (carry != 0 or i < length) { + carry +%= 256 *% b58[b58Pointer]; + b58[b58Pointer] := carry % 58; + carry /= 58; + i += 1; + if (b58Pointer == 0) break inner; + b58Pointer -= 1; + }; + assert (carry == 0); + length := i; + pos += 1; + rem += 1; + }; + + // Process full batches of 7 bytes: b58 = b58 * 256^7 + v. + // 256^7 = 72_057_594_037_927_936. Max carry < 2^62, fits in Nat64. + while (pos < inputSize) { + var carry : Nat64 = Nat64.fromIntWrap(input[pos].toNat()) << 48 | Nat64.fromIntWrap(input[pos + 1].toNat()) << 40 | Nat64.fromIntWrap(input[pos + 2].toNat()) << 32 | Nat64.fromIntWrap(input[pos + 3].toNat()) << 24 | Nat64.fromIntWrap(input[pos + 4].toNat()) << 16 | Nat64.fromIntWrap(input[pos + 5].toNat()) << 8 | Nat64.fromIntWrap(input[pos + 6].toNat()); - while (inputPointer < input.size()) { - var carry : Nat32 = input[inputPointer].toNat16().toNat32(); var i : Nat = 0; - // Apply "b58 = b58 * 256 + ch". - var b58Pointer : Nat = b58.size() - 1; - label reverseIter while (carry != 0 or i < length) { - carry += 256 * b58[b58Pointer].toNat16().toNat32(); - b58[b58Pointer] := Nat8.fromNat((carry % 58).toNat()); + var b58Pointer : Nat = size - 1; + label inner while (carry != 0 or i < length) { + carry +%= 72_057_594_037_927_936 *% b58[b58Pointer].toNat32().toNat64(); + b58[b58Pointer] := (carry % 58).toNat32().toNat16(); carry /= 58; i += 1; - if (b58Pointer == 0) { - break reverseIter; - }; + if (b58Pointer == 0) break inner; b58Pointer -= 1; }; assert (carry == 0); length := i; - inputPointer += 1; + pos += 7; }; // Skip leading zeroes in base58 result. var b58Pointer : Nat = size - length; - while (b58Pointer < b58.size() and b58[b58Pointer] == 0) { b58Pointer += 1 }; + while (b58Pointer < size and b58[b58Pointer] == 0) { + b58Pointer += 1; + }; let outputBytes = Array.tabulate( - zeroes + b58.size() - b58Pointer, + zeroes + size - b58Pointer, func(i) { if (i < zeroes) { 0x31 : Nat8; } else { - base58Alphabet[b58[i + b58Pointer - zeroes].toNat()]; + base58Alphabet[b58[b58Pointer + i - zeroes].toNat()]; }; }, ); diff --git a/src/Bech32.mo b/src/Bech32.mo index 46357b5..57800cf 100644 --- a/src/Bech32.mo +++ b/src/Bech32.mo @@ -199,9 +199,7 @@ module { Array.tabulate( 6, func(i) { - Nat8.fromIntWrap( - ((mod >> (5 * (5 - Nat32.fromIntWrap(i)))) & 31).toNat() - ); + ((mod >> (5 * (5 - Nat32.fromIntWrap(i)))) & 31).toNat16().toNat8(); }, ); }; @@ -229,7 +227,7 @@ module { var c : Nat32 = 1; for (value in values.values()) { - let c0 : Nat8 = Nat8.fromIntWrap((c >> 25).toNat()); + let c0 : Nat8 = (c >> 25).toNat16().toNat8(); c := ((c & 0x1ffffff) << 5) ^ value.toNat16().toNat32(); // Conditionally add in coefficients of the generator polynomial. diff --git a/src/Segwit.mo b/src/Segwit.mo index 3796f1e..ab9de59 100644 --- a/src/Segwit.mo +++ b/src/Segwit.mo @@ -1,10 +1,10 @@ -import List "mo:core/List"; +import Array "mo:core/Array"; import Nat "mo:core/Nat"; import Nat16 "mo:core/Nat16"; import Nat32 "mo:core/Nat32"; import Nat8 "mo:core/Nat8"; -import Runtime "mo:core/Runtime"; -import { type Result; type Iter } "mo:core/Types"; +import { type Result } "mo:core/Types"; +import VarArray "mo:core/VarArray"; import Bech32 "Bech32"; @@ -18,14 +18,9 @@ module { // Convert a Witness Program to a SegWit Address. public func encode(hrp : Text, { version; program } : WitnessProgram) : Result { - let bech32Input = List.empty(); - bech32Input.add(version); - - switch (convertBits(program.values(), bech32Input, 8, 5, true)) { - case (#err(msg)) { - return #err(msg); - }; - case _ {}; + let converted = switch (convertBits(program, 0, 8, 5, true)) { + case (#err(msg)) return #err(msg); + case (#ok(c)) c; }; let encoding : Bech32.Encoding = if (version > 0) { @@ -36,7 +31,7 @@ module { let bech32Result : Text = Bech32.encode( hrp, - bech32Input.toArray(), + [[version] : [Nat8], converted].flatten(), encoding, ); @@ -68,69 +63,60 @@ module { return #err("Invalid data length."); }; - // Split into version and program. - let dataIter : Iter = data.values(); - let version : Nat8 = switch (dataIter.next()) { - case (?val) { - val; - }; - case _ { - Runtime.trap("unreachable"); - }; + let version : Nat8 = data[0]; + + let convertedData = switch (convertBits(data, 1, 5, 8, false)) { + case (#ok(d)) d; + case _ return #err("Convert bits failed."); }; - let convertedData = List.empty(); - switch (convertBits(dataIter, convertedData, 5, 8, false)) { - case (#ok) { - let convertedDataSize : Nat = convertedData.size(); - - if (convertedDataSize < 2 or convertedDataSize > 40) { - return #err("Wrong output size."); - }; - - if (data[0] > 16) { - return #err("Invalid witness version."); - }; - - if ( - data[0] == 0 and convertedDataSize != 20 and convertedDataSize != 32 - ) { - return #err("Program size does not match witness version."); - }; - - if ( - data[0] == 0 and encoding != #BECH32 or - data[0] != 0 and encoding != #BECH32M - ) { - return #err("Encoding does not match witness version."); - }; - - return #ok(decodedHrp, { version; program = convertedData.toArray() }); - }; - case _ { - return #err("Convert bits failed."); - }; + let convertedDataSize : Nat = convertedData.size(); + + if (convertedDataSize < 2 or convertedDataSize > 40) { + return #err("Wrong output size."); + }; + + if (data[0] > 16) { + return #err("Invalid witness version."); + }; + + if ( + data[0] == 0 and convertedDataSize != 20 and convertedDataSize != 32 + ) { + return #err("Program size does not match witness version."); + }; + + if ( + data[0] == 0 and encoding != #BECH32 or + data[0] != 0 and encoding != #BECH32M + ) { + return #err("Encoding does not match witness version."); }; + + #ok(decodedHrp, { version; program = convertedData }); }; // Convert between two bases that are power of 2. func convertBits( - data : Iter, - output : List.List, + data : [Nat8], + start : Nat, from : Nat32, to : Nat32, pad : Bool, - ) : Result<(), Text> { + ) : Result<[Nat8], Text> { var acc : Nat32 = 0; var bits : Nat32 = 0; let maxv : Nat32 = (1 << to) - 1; + let output = VarArray.repeat(0, data.size() * from.toNat() / to.toNat() + 1); + var outputLen : Nat = 0; - for (value in data) { - let v : Nat32 = value.toNat16().toNat32(); + var pos = start; + while (pos < data.size()) { + let v : Nat32 = data[pos].toNat16().toNat32(); if ((v >> from) != 0) { - return #err("Invalid input value: " # value.toNat().toText()); + return #err("Invalid input value: " # data[pos].toNat().toText()); }; acc := (acc << from) | v; @@ -138,26 +124,21 @@ module { while (bits >= to) { bits -= to; - output.add( - Nat8.fromIntWrap( - ((acc >> bits) & maxv).toNat() - ) - ); + output[outputLen] := ((acc >> bits) & maxv).toNat16().toNat8(); + outputLen += 1; }; + pos += 1; }; if (pad) { if (bits > 0) { - output.add( - Nat8.fromIntWrap( - ((acc << (to - bits)) & maxv).toNat() - ) - ); + output[outputLen] := ((acc << (to - bits)) & maxv).toNat16().toNat8(); + outputLen += 1; }; } else if (bits >= from or ((acc << (to - bits)) & maxv) != 0) { return #err("Invalid Padding"); }; - return #ok; + #ok(Array.tabulate(outputLen, func(i) = output[i])); }; }; From dfa41e2f979e747df0e7248e3bf607d6b9cd35c6 Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Thu, 16 Apr 2026 22:33:48 +0200 Subject: [PATCH 19/34] Fix ByteUtils.read() for reverse order and count = 0 --- src/ByteUtils.mo | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ByteUtils.mo b/src/ByteUtils.mo index d2cb751..e6af3a1 100644 --- a/src/ByteUtils.mo +++ b/src/ByteUtils.mo @@ -17,6 +17,8 @@ module { reverse : Bool, ) : ?[Nat8] { do ? { + if (count == 0) return ?[]; + let readData : [var Nat8] = VarArray.repeat(0, count); if (reverse) { var nextReadIndex : Nat = count - 1; From 318664f6bd7476bf274a1b64a0a7364e704a4fc3 Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Thu, 16 Apr 2026 22:38:42 +0200 Subject: [PATCH 20/34] Define and use tiny arrayToText function (for better re-use) --- src/Base58.mo | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Base58.mo b/src/Base58.mo index 3488bff..fbe83ef 100755 --- a/src/Base58.mo +++ b/src/Base58.mo @@ -38,6 +38,13 @@ module { 255,255,255,255,255,255,255,255, ]; + func arrayToText(arr : [Nat8]) : Text { + switch (Blob.fromArray(arr).decodeUtf8()) { + case (?t) t; + case null Runtime.trap("unreachable"); + }; + }; + // Convert the given Base58 input to Base256. public func decode(input_ : Text) : [Nat8] { let input : Blob = Text.encodeUtf8(input_); @@ -222,9 +229,7 @@ module { }; }, ); - switch (Blob.fromArray(outputBytes).decodeUtf8()) { - case (?t) t; - case null Runtime.trap("unreachable"); - }; + + arrayToText(outputBytes); }; }; From ae8aa51e435bde659e9760d8a27422d0c9eb8b6c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 20:43:25 +0000 Subject: [PATCH 21/34] chore(bench): update benchmark.md [skip ci] --- benchmark.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/benchmark.md b/benchmark.md index f2dbbbf..095aa36 100644 --- a/benchmark.md +++ b/benchmark.md @@ -21,7 +21,7 @@ Garbage Collection: ${\color{gray}0\\%}$ | | len 0 | len 10 | len 32 | len 64 | len 128 | | :----- | ----: | -----: | -----: | ------: | ------: | -| encode | 1_193 | 10_490 | 44_319 | 127_586 | 429_639 | +| encode | 1_202 | 10_499 | 44_328 | 127_595 | 429_648 | | decode | 1_092 | 10_192 | 34_994 | 106_525 | 347_882 | @@ -62,7 +62,7 @@ Garbage Collection: ${\color{gray}0\\%}$ | | len 0 | len 10 | len 32 | len 64 | len 128 | | :----- | -----: | -----: | -----: | ------: | ------: | -| encode | 40_619 | 52_094 | 96_434 | 205_225 | 546_308 | +| encode | 40_628 | 52_103 | 96_443 | 205_234 | 546_317 | | decode | 40_702 | 48_521 | 80_562 | 164_580 | 430_823 | @@ -191,8 +191,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | 2 utxos | 4 utxos | | :------ | ------: | ------: | -| build | 230_594 | 238_662 | -| sighash | 669_664 | 677_795 | +| build | 230_660 | 238_728 | +| sighash | 669_752 | 677_883 | **Heap** @@ -273,7 +273,7 @@ Garbage Collection: ${\color{gray}0\\%}$ | | sample 0 | sample 1 | | :----------------- | ----------: | ----------: | -| DER+verify | 307_343_230 | 306_581_317 | +| DER+verify | 307_343_274 | 306_581_361 | | verify (preparsed) | 306_815_024 | 306_097_902 | From b286978bffbec65733638c2f5f43e0b40df11c86 Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Fri, 17 Apr 2026 07:02:58 +0200 Subject: [PATCH 22/34] Use arrayToText helper in Bech32.mo --- src/Bech32.mo | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Bech32.mo b/src/Bech32.mo index 57800cf..293f211 100644 --- a/src/Bech32.mo +++ b/src/Bech32.mo @@ -49,6 +49,13 @@ module { 3, 16, 11, 28, 12, 14, 6, 4, 2, 255, 255, 255, 255, 255 ]; + func arrayToText(arr : [Nat8]) : Text { + switch (Blob.fromArray(arr).decodeUtf8()) { + case (?t) t; + case null Runtime.trap("unreachable"); + }; + }; + // Encode input in Bech32 or a Bech32m. public func encode(hrp : Text, values : [Nat8], encoding : Encoding) : Text { assert hrp.size() > 0; @@ -72,10 +79,7 @@ module { assert output.size() <= 90; - switch (Blob.fromArray(output).decodeUtf8()) { - case (?t) t; - case null Runtime.trap("unreachable"); - }; + arrayToText(output); }; // Decode given text as Bech32 or Bech32m. From 370d3bb35187aa8a0058e7b1bfafd1b67c0eb704 Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Fri, 17 Apr 2026 07:07:05 +0200 Subject: [PATCH 23/34] Make Segwit.decode() fail on invalid witness version --- src/Segwit.mo | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Segwit.mo b/src/Segwit.mo index ab9de59..d1c3374 100644 --- a/src/Segwit.mo +++ b/src/Segwit.mo @@ -65,6 +65,10 @@ module { let version : Nat8 = data[0]; + if (version > 16) { + return #err("Invalid witness version."); + }; + let convertedData = switch (convertBits(data, 1, 5, 8, false)) { case (#ok(d)) d; case _ return #err("Convert bits failed."); @@ -76,19 +80,19 @@ module { return #err("Wrong output size."); }; - if (data[0] > 16) { + if (version > 16) { return #err("Invalid witness version."); }; if ( - data[0] == 0 and convertedDataSize != 20 and convertedDataSize != 32 + version == 0 and convertedDataSize != 20 and convertedDataSize != 32 ) { return #err("Program size does not match witness version."); }; if ( - data[0] == 0 and encoding != #BECH32 or - data[0] != 0 and encoding != #BECH32M + version == 0 and encoding != #BECH32 or + version != 0 and encoding != #BECH32M ) { return #err("Encoding does not match witness version."); }; From 3e4d74cf243feef5a9b99579c9cb274b0fc62cb3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 05:09:04 +0000 Subject: [PATCH 24/34] chore(bench): update benchmark.md [skip ci] --- benchmark.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/benchmark.md b/benchmark.md index 095aa36..a7dbdca 100644 --- a/benchmark.md +++ b/benchmark.md @@ -103,8 +103,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | len 0 | len 5 | len 20 | len 32 | | :------------- | -----: | -----: | -----: | -----: | -| encode bech32 | 11_965 | 15_521 | 25_937 | 34_281 | -| encode bech32m | 11_997 | 15_553 | 25_969 | 34_313 | +| encode bech32 | 11_973 | 15_529 | 25_945 | 34_289 | +| encode bech32m | 12_005 | 15_561 | 25_977 | 34_321 | | decode bech32 | 10_685 | 15_052 | 27_530 | 37_556 | | decode bech32m | 10_759 | 15_150 | 27_604 | 37_618 | @@ -355,8 +355,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | bc v0/20 | bc v0/32 | bc v1/32 | tb v0/20 | tb v0/32 | tb v1/32 | | :----- | -------: | -------: | -------: | -------: | -------: | -------: | -| encode | 103_139 | 150_473 | 150_445 | 103_075 | 150_517 | 150_505 | -| decode | 49_388 | 72_072 | 72_063 | 49_356 | 72_112 | 72_087 | +| encode | 103_035 | 150_369 | 150_341 | 102_971 | 150_413 | 150_401 | +| decode | 49_276 | 71_960 | 71_951 | 49_244 | 72_000 | 71_975 | **Heap** From b2619975dc33df3b415116e2a43b5d7e83e94d73 Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Sat, 18 Apr 2026 15:04:35 +0200 Subject: [PATCH 25/34] Update CHANGELOG --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9a97f0..0ae76ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,14 @@ # Motoko `bitcoin` changelog -## 1.0.0 +## Next +* Optimize (de)serializations in Bech32, Base58, Segwit * Bugfix: Taproot sighash now uses actual transaction values instead of hardcoded locktime=0 and version=2 (#14) -* Migrate code from `base` to `core` +* *Breaking:* Reject BIP32 paths with double-slashes in `Bip32.mo` (bugfix) * *Breaking:* Remove `toBytes` function in `bitcoin/TxOutput.mo` (use class method instead) * *Breaking:* Add length assertions inside `Bech32.encode()` * *Breaking*: Lowercase character range in `Bech32.mo` was incorrect (bugfix) -* *Breaking:* Reject BIP32 paths with double-slashes in `Bip32.mo` (bugfix) +* Migrate code from `base` to `core` ## 0.1.1 From 24d2be2741e26239aa537d6783666d35fa472031 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 13:05:58 +0000 Subject: [PATCH 26/34] chore(bench): update benchmark.md [skip ci] --- benchmark.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/benchmark.md b/benchmark.md index a7dbdca..aa36674 100644 --- a/benchmark.md +++ b/benchmark.md @@ -191,8 +191,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | 2 utxos | 4 utxos | | :------ | ------: | ------: | -| build | 230_660 | 238_728 | -| sighash | 669_752 | 677_883 | +| build | 230_666 | 238_734 | +| sighash | 669_758 | 677_889 | **Heap** @@ -207,8 +207,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | 2 utxos | 4 utxos | | :------ | --------: | --------: | -| build | 14.17 KiB | 14.64 KiB | -| sighash | 30.09 KiB | 30.56 KiB | +| build | 14.18 KiB | 14.65 KiB | +| sighash | 30.1 KiB | 30.57 KiB | From 905e15e8249854ec032c5756896ce1b692707daf Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Sat, 18 Apr 2026 15:35:41 +0200 Subject: [PATCH 27/34] Remove duplicate version check in Segwit.mo --- src/Segwit.mo | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Segwit.mo b/src/Segwit.mo index d1c3374..66a7e17 100644 --- a/src/Segwit.mo +++ b/src/Segwit.mo @@ -80,10 +80,6 @@ module { return #err("Wrong output size."); }; - if (version > 16) { - return #err("Invalid witness version."); - }; - if ( version == 0 and convertedDataSize != 20 and convertedDataSize != 32 ) { From cc2106108c0a8bb7ff554a00a0f195fbdb8564d6 Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Sat, 18 Apr 2026 15:37:02 +0200 Subject: [PATCH 28/34] Pass through convertBits error message in Segwit.mo --- src/Segwit.mo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Segwit.mo b/src/Segwit.mo index 66a7e17..10344b0 100644 --- a/src/Segwit.mo +++ b/src/Segwit.mo @@ -70,8 +70,8 @@ module { }; let convertedData = switch (convertBits(data, 1, 5, 8, false)) { + case (#err(msg)) return #err(msg); case (#ok(d)) d; - case _ return #err("Convert bits failed."); }; let convertedDataSize : Nat = convertedData.size(); From e7399b61c028448e7ea5a1836b48eec7fdf42820 Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Sat, 18 Apr 2026 20:56:36 +0200 Subject: [PATCH 29/34] Make bench.yml workflow handle fork PRs (from coderabbit) --- .github/workflows/bench.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index c805840..4561f73 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -44,6 +44,7 @@ jobs: mops bench > benchmark.md - name: Commit benchmark.md if changed + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository run: | if git diff --quiet -- benchmark.md; then echo "No changes in benchmark.md" From 6584ebed6ccd203b63a46677adf3cb2cbb2180fb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 18:58:36 +0000 Subject: [PATCH 30/34] chore(bench): update benchmark.md [skip ci] --- benchmark.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/benchmark.md b/benchmark.md index aa36674..360c19e 100644 --- a/benchmark.md +++ b/benchmark.md @@ -192,7 +192,7 @@ Garbage Collection: ${\color{gray}0\\%}$ | | 2 utxos | 4 utxos | | :------ | ------: | ------: | | build | 230_666 | 238_734 | -| sighash | 669_758 | 677_889 | +| sighash | 669_821 | 677_889 | **Heap** @@ -355,8 +355,8 @@ Garbage Collection: ${\color{gray}0\\%}$ | | bc v0/20 | bc v0/32 | bc v1/32 | tb v0/20 | tb v0/32 | tb v1/32 | | :----- | -------: | -------: | -------: | -------: | -------: | -------: | -| encode | 103_035 | 150_369 | 150_341 | 102_971 | 150_413 | 150_401 | -| decode | 49_276 | 71_960 | 71_951 | 49_244 | 72_000 | 71_975 | +| encode | 103_034 | 150_368 | 150_340 | 102_970 | 150_412 | 150_400 | +| decode | 49_275 | 71_959 | 71_950 | 49_243 | 71_999 | 71_974 | **Heap** From 3b43a8fb63f2f40cee6f1f4cfd3cb0c3e2d9a77b Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Sat, 18 Apr 2026 21:00:01 +0200 Subject: [PATCH 31/34] Remove bench workflow and benchmark.md for upstream merging --- .github/workflows/bench.yml | 59 ------ benchmark.md | 378 ------------------------------------ 2 files changed, 437 deletions(-) delete mode 100644 .github/workflows/bench.yml delete mode 100644 benchmark.md diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml deleted file mode 100644 index 4561f73..0000000 --- a/.github/workflows/bench.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Benchmarks - -on: - push: - branches: [ main ] - pull_request: - -permissions: - contents: write - -jobs: - bench: - name: Run benches and commit markdown - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - persist-credentials: true - fetch-depth: 0 - ref: ${{ github.head_ref || github.ref_name }} - - - name: Install dfx and IC SDK - uses: dfinity/setup-dfx@main - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '22' - - - name: Install mops and PocketIC - run: | - npm install -g ic-mops @dfinity/pic - mops --version - - - name: Install dependencies (mops) - run: mops install - - - name: Setup Motoko toolchain (mops) - run: mops toolchain init - - - name: Run benches to markdown - run: | - mops bench > benchmark.md - - - name: Commit benchmark.md if changed - if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository - run: | - if git diff --quiet -- benchmark.md; then - echo "No changes in benchmark.md" - else - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - BRANCH_NAME="${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}" - git add benchmark.md - git commit -m "chore(bench): update benchmark.md [skip ci]" - git pull --rebase origin "$BRANCH_NAME" - git push origin HEAD:"$BRANCH_NAME" - fi diff --git a/benchmark.md b/benchmark.md deleted file mode 100644 index 360c19e..0000000 --- a/benchmark.md +++ /dev/null @@ -1,378 +0,0 @@ -# Benchmark Results - - - -
- -bench/base58.bench.mo $({\color{gray}0\%})$ - -### Base58 encode/decode - -_Benchmark Base58 encode/decode across input sizes_ - - -Instructions: ${\color{gray}0\\%}$ -Heap: ${\color{gray}0\\%}$ -Stable Memory: ${\color{gray}0\\%}$ -Garbage Collection: ${\color{gray}0\\%}$ - - -**Instructions** - -| | len 0 | len 10 | len 32 | len 64 | len 128 | -| :----- | ----: | -----: | -----: | ------: | ------: | -| encode | 1_202 | 10_499 | 44_328 | 127_595 | 429_648 | -| decode | 1_092 | 10_192 | 34_994 | 106_525 | 347_882 | - - -**Heap** - -| | len 0 | len 10 | len 32 | len 64 | len 128 | -| :----- | ----: | -----: | -----: | -----: | ------: | -| encode | 272 B | 272 B | 272 B | 272 B | 272 B | -| decode | 272 B | 272 B | 272 B | 272 B | 272 B | - - -**Garbage Collection** - -| | len 0 | len 10 | len 32 | len 64 | len 128 | -| :----- | ----: | -----: | -----: | -------: | -------: | -| encode | 364 B | 500 B | 800 B | 1.21 KiB | 2.07 KiB | -| decode | 348 B | 424 B | 600 B | 856 B | 1.34 KiB | - - -
- -
- -bench/base58check.bench.mo $({\color{gray}0\%})$ - -### Base58Check encode/decode - -_Benchmark Base58Check encode/decode across input sizes_ - - -Instructions: ${\color{gray}0\\%}$ -Heap: ${\color{gray}0\\%}$ -Stable Memory: ${\color{gray}0\\%}$ -Garbage Collection: ${\color{gray}0\\%}$ - - -**Instructions** - -| | len 0 | len 10 | len 32 | len 64 | len 128 | -| :----- | -----: | -----: | -----: | ------: | ------: | -| encode | 40_628 | 52_103 | 96_443 | 205_234 | 546_317 | -| decode | 40_702 | 48_521 | 80_562 | 164_580 | 430_823 | - - -**Heap** - -| | len 0 | len 10 | len 32 | len 64 | len 128 | -| :----- | ----: | -----: | -----: | -----: | ------: | -| encode | 272 B | 272 B | 272 B | 272 B | 272 B | -| decode | 272 B | 272 B | 272 B | 272 B | 272 B | - - -**Garbage Collection** - -| | len 0 | len 10 | len 32 | len 64 | len 128 | -| :----- | -------: | -------: | -------: | -------: | -------: | -| encode | 3.91 KiB | 4.16 KiB | 4.63 KiB | 5.29 KiB | 6.66 KiB | -| decode | 3.87 KiB | 3.98 KiB | 4.24 KiB | 4.61 KiB | 5.36 KiB | - - -
- -
- -bench/bech32.bench.mo $({\color{gray}0\%})$ - -### Bech32 vs Bech32m - -_Compare Bech32 and Bech32m encoding across sizes_ - - -Instructions: ${\color{gray}0\\%}$ -Heap: ${\color{gray}0\\%}$ -Stable Memory: ${\color{gray}0\\%}$ -Garbage Collection: ${\color{gray}0\\%}$ - - -**Instructions** - -| | len 0 | len 5 | len 20 | len 32 | -| :------------- | -----: | -----: | -----: | -----: | -| encode bech32 | 11_973 | 15_529 | 25_945 | 34_289 | -| encode bech32m | 12_005 | 15_561 | 25_977 | 34_321 | -| decode bech32 | 10_685 | 15_052 | 27_530 | 37_556 | -| decode bech32m | 10_759 | 15_150 | 27_604 | 37_618 | - - -**Heap** - -| | len 0 | len 5 | len 20 | len 32 | -| :------------- | ----: | ----: | -----: | -----: | -| encode bech32 | 272 B | 272 B | 272 B | 272 B | -| encode bech32m | 272 B | 272 B | 272 B | 272 B | -| decode bech32 | 272 B | 272 B | 272 B | 272 B | -| decode bech32m | 272 B | 272 B | 272 B | 272 B | - - -**Garbage Collection** - -| | len 0 | len 5 | len 20 | len 32 | -| :------------- | ----: | ----: | -------: | -------: | -| encode bech32 | 840 B | 908 B | 1.09 KiB | 1.26 KiB | -| encode bech32m | 840 B | 908 B | 1.09 KiB | 1.26 KiB | -| decode bech32 | 804 B | 932 B | 1.2 KiB | 1.44 KiB | -| decode bech32m | 804 B | 932 B | 1.2 KiB | 1.44 KiB | - - -
- -
- -bench/bip32.bench.mo $({\color{gray}0\%})$ - -### BIP32 derivePath: text vs array - -_Compare path representations for public derivation_ - - -Instructions: ${\color{gray}0\\%}$ -Heap: ${\color{gray}0\\%}$ -Stable Memory: ${\color{gray}0\\%}$ -Garbage Collection: ${\color{gray}0\\%}$ - - -**Instructions** - -| | depth 3 | depth 4 | depth 5 | -| :---- | ----------: | ----------: | ----------: | -| text | 544_358_509 | 725_157_063 | 909_593_270 | -| array | 544_254_818 | 725_092_034 | 909_518_890 | - - -**Heap** - -| | depth 3 | depth 4 | depth 5 | -| :---- | ------: | ------: | ------: | -| text | 272 B | 272 B | 272 B | -| array | 272 B | 272 B | 272 B | - - -**Garbage Collection** - -| | depth 3 | depth 4 | depth 5 | -| :---- | --------: | --------: | --------: | -| text | 13.37 MiB | 17.81 MiB | 22.34 MiB | -| array | 13.37 MiB | 17.81 MiB | 22.34 MiB | - - -
- -
- -bench/bitcoin_tx.bench.mo $({\color{gray}0\%})$ - -### Bitcoin tx: build vs sighash - -_Compare building a simple tx vs computing P2PKH sighash_ - - -Instructions: ${\color{gray}0\\%}$ -Heap: ${\color{gray}0\\%}$ -Stable Memory: ${\color{gray}0\\%}$ -Garbage Collection: ${\color{gray}0\\%}$ - - -**Instructions** - -| | 2 utxos | 4 utxos | -| :------ | ------: | ------: | -| build | 230_666 | 238_734 | -| sighash | 669_821 | 677_889 | - - -**Heap** - -| | 2 utxos | 4 utxos | -| :------ | ------: | ------: | -| build | 272 B | 272 B | -| sighash | 272 B | 272 B | - - -**Garbage Collection** - -| | 2 utxos | 4 utxos | -| :------ | --------: | --------: | -| build | 14.18 KiB | 14.65 KiB | -| sighash | 30.1 KiB | 30.57 KiB | - - -
- -
- -bench/ec_arith.bench.mo $({\color{gray}0\%})$ - -### EC scalar mul: base vs arbitrary point - -_Compare scalar multiplication using generator vs arbitrary point_ - - -Instructions: ${\color{gray}0\\%}$ -Heap: ${\color{gray}0\\%}$ -Stable Memory: ${\color{gray}0\\%}$ -Garbage Collection: ${\color{gray}0\\%}$ - - -**Instructions** - -| | k small | k medium | k large | -| :------- | ---------: | ---------: | ---------: | -| mulBase | 10_058_884 | 17_922_235 | 36_223_854 | -| mulPoint | 10_053_853 | 17_917_924 | 36_218_103 | - - -**Heap** - -| | k small | k medium | k large | -| :------- | ------: | -------: | ------: | -| mulBase | 272 B | 272 B | 272 B | -| mulPoint | 272 B | 272 B | 272 B | - - -**Garbage Collection** - -| | k small | k medium | k large | -| :------- | ---------: | ---------: | ---------: | -| mulBase | 322.99 KiB | 513.06 KiB | 976.04 KiB | -| mulPoint | 322.36 KiB | 512.43 KiB | 975.4 KiB | - - -
- -
- -bench/ecdsa_verify.bench.mo $({\color{gray}0\%})$ - -### ECDSA verify: DER vs raw (DER decode cost) - -_Compare verifying using DER decode per run vs reusing parsed signature_ - - -Instructions: ${\color{gray}0\\%}$ -Heap: ${\color{gray}0\\%}$ -Stable Memory: ${\color{gray}0\\%}$ -Garbage Collection: ${\color{gray}0\\%}$ - - -**Instructions** - -| | sample 0 | sample 1 | -| :----------------- | ----------: | ----------: | -| DER+verify | 307_343_274 | 306_581_361 | -| verify (preparsed) | 306_815_024 | 306_097_902 | - - -**Heap** - -| | sample 0 | sample 1 | -| :----------------- | -------: | -------: | -| DER+verify | 272 B | 272 B | -| verify (preparsed) | 272 B | 272 B | - - -**Garbage Collection** - -| | sample 0 | sample 1 | -| :----------------- | -------: | -------: | -| DER+verify | 7.69 MiB | 7.66 MiB | -| verify (preparsed) | 7.67 MiB | 7.65 MiB | - - -
- -
- -bench/hash_hmac.bench.mo $({\color{gray}0\%})$ - -### HMAC: SHA256 vs SHA512 - -_Compare HMAC-SHA256 and HMAC-SHA512 across message sizes_ - - -Instructions: ${\color{gray}0\\%}$ -Heap: ${\color{gray}0\\%}$ -Stable Memory: ${\color{gray}0\\%}$ -Garbage Collection: ${\color{gray}0\\%}$ - - -**Instructions** - -| | len 0 | len 32 | len 64 | len 256 | -| :---------- | ------: | ------: | ------: | ------: | -| HMAC-SHA256 | 75_746 | 79_240 | 86_915 | 118_397 | -| HMAC-SHA512 | 118_026 | 121_255 | 123_599 | 152_298 | - - -**Heap** - -| | len 0 | len 32 | len 64 | len 256 | -| :---------- | ----: | -----: | -----: | ------: | -| HMAC-SHA256 | 272 B | 272 B | 272 B | 272 B | -| HMAC-SHA512 | 272 B | 272 B | 272 B | 272 B | - - -**Garbage Collection** - -| | len 0 | len 32 | len 64 | len 256 | -| :---------- | -------: | -------: | -------: | -------: | -| HMAC-SHA256 | 4.73 KiB | 4.73 KiB | 4.73 KiB | 4.73 KiB | -| HMAC-SHA512 | 6.78 KiB | 6.92 KiB | 6.97 KiB | 6.88 KiB | - - -
- -
- -bench/segwit.bench.mo $({\color{gray}0\%})$ - -### SegWit (address encode/decode) - -_Benchmark SegWit Bech32/Bech32m address encode/decode for common versions and program lengths_ - - -Instructions: ${\color{gray}0\\%}$ -Heap: ${\color{gray}0\\%}$ -Stable Memory: ${\color{gray}0\\%}$ -Garbage Collection: ${\color{gray}0\\%}$ - - -**Instructions** - -| | bc v0/20 | bc v0/32 | bc v1/32 | tb v0/20 | tb v0/32 | tb v1/32 | -| :----- | -------: | -------: | -------: | -------: | -------: | -------: | -| encode | 103_034 | 150_368 | 150_340 | 102_970 | 150_412 | 150_400 | -| decode | 49_275 | 71_959 | 71_950 | 49_243 | 71_999 | 71_974 | - - -**Heap** - -| | bc v0/20 | bc v0/32 | bc v1/32 | tb v0/20 | tb v0/32 | tb v1/32 | -| :----- | -------: | -------: | -------: | -------: | -------: | -------: | -| encode | 272 B | 272 B | 272 B | 272 B | 272 B | 272 B | -| decode | 272 B | 272 B | 272 B | 272 B | 272 B | 272 B | - - -**Garbage Collection** - -| | bc v0/20 | bc v0/32 | bc v1/32 | tb v0/20 | tb v0/32 | tb v1/32 | -| :----- | -------: | -------: | -------: | -------: | -------: | -------: | -| encode | 3.21 KiB | 4.21 KiB | 4.21 KiB | 3.21 KiB | 4.21 KiB | 4.21 KiB | -| decode | 1.72 KiB | 2.21 KiB | 2.21 KiB | 1.72 KiB | 2.21 KiB | 2.21 KiB | - - -
From 57f33b53109cfa997751d7af2fcbd4ef24f3ef8b Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Sun, 19 Apr 2026 11:54:34 +0200 Subject: [PATCH 32/34] Trivial optimization in Jacobi.mo for curves with a = 0 --- src/ec/Jacobi.mo | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ec/Jacobi.mo b/src/ec/Jacobi.mo index 8d4c2f7..484f3e4 100644 --- a/src/ec/Jacobi.mo +++ b/src/ec/Jacobi.mo @@ -237,7 +237,9 @@ module { let YYYY : Int = YY * YY % p; let ZZ : Int = Z1 * Z1 % p; let S : Int = 2 * ((X1 + YY) ** 2 - XX - YYYY) % p; - let M : Int = (3 * XX + a * ZZ * ZZ) % p; + let M : Int = if (a == 0) { 3 * XX % p } else { + (3 * XX + a * ZZ * ZZ) % p; + }; let T : Int = (M * M - 2 * S) % p; let Y3 = (M * (S - T) - 8 * YYYY) % p; From 6cc9ed48301e78e47983f55a8cae1026cc73ddc5 Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Mon, 20 Apr 2026 09:34:34 +0200 Subject: [PATCH 33/34] Replace ** 2 with explicit self-multiplication in Jacobi.mo But only in the hottest path, where it is worth it, to not affect readability elsewhere. --- src/ec/Jacobi.mo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ec/Jacobi.mo b/src/ec/Jacobi.mo index 484f3e4..44cfbf8 100644 --- a/src/ec/Jacobi.mo +++ b/src/ec/Jacobi.mo @@ -236,14 +236,14 @@ module { let (XX, YY) = (X1 * X1 % p, Y1 * Y1 % p); let YYYY : Int = YY * YY % p; let ZZ : Int = Z1 * Z1 % p; - let S : Int = 2 * ((X1 + YY) ** 2 - XX - YYYY) % p; + let S : Int = (X1 + YY) |> 2 * (_ * _ - XX - YYYY) % p; let M : Int = if (a == 0) { 3 * XX % p } else { (3 * XX + a * ZZ * ZZ) % p; }; let T : Int = (M * M - 2 * S) % p; let Y3 = (M * (S - T) - 8 * YYYY) % p; - let Z3 = ((Y1 + Z1) ** 2 - YY - ZZ) % p; + let Z3 = (Y1 + Z1) |> (_ * _ - YY - ZZ) % p; return (T, Y3, Z3); }; @@ -308,7 +308,7 @@ module { }; let V : Int = X1 * I; - let X3 : Int = (r ** 2 - J - 2 * V) % p; + let X3 : Int = (r * r - J - 2 * V) % p; let Y3 : Int = (r * (V - X3) - 2 * Y1 * J) % p; let Z3 : Int = 2 * H % p; From 5dbae5ad39b49fcbade532f493a439a5d5966f42 Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Tue, 21 Apr 2026 17:35:08 +0200 Subject: [PATCH 34/34] Two small fixes as per comments --- src/Base58.mo | 2 +- src/Segwit.mo | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Base58.mo b/src/Base58.mo index fbe83ef..55674d5 100755 --- a/src/Base58.mo +++ b/src/Base58.mo @@ -172,7 +172,7 @@ module { let size : Nat = bytesCount * 138 / 100 + 1; let b58 : [var Nat16] = VarArray.repeat(0, size); - // Process leading remainder bytes (remainingBytes % 7) one at a time. + // Process leading remainder bytes (bytesCount % 7) one at a time. let remainder = bytesCount % 7; var rem : Nat = 0; while (rem < remainder) { diff --git a/src/Segwit.mo b/src/Segwit.mo index 10344b0..649f520 100644 --- a/src/Segwit.mo +++ b/src/Segwit.mo @@ -108,11 +108,12 @@ module { var acc : Nat32 = 0; var bits : Nat32 = 0; let maxv : Nat32 = (1 << to) - 1; - let output = VarArray.repeat(0, data.size() * from.toNat() / to.toNat() + 1); + let dataSize = data.size(); + let output = VarArray.repeat(0, (dataSize - start) * from.toNat() / to.toNat() + 1); var outputLen : Nat = 0; var pos = start; - while (pos < data.size()) { + while (pos < dataSize) { let v : Nat32 = data[pos].toNat16().toNat32(); if ((v >> from) != 0) {