Quantumroot: Quantum-Secure Vaults for Bitcoin Cash
Today I’m sharing a developer preview of Quantumroot: a new kind of vault offering full 256-bit classical, 128-bit quantum security strength, without relying on new cryptography – it uses only the mining-proven SHA256 algorithm.
Quantumroot is an ultra-efficient, post-quantum vault for Bitcoin Cash. It's optimized for business and savings use cases:
- With cross-input aggregation via introspection, sweep transactions are 15% smaller per additional input than today’s single-signature, "Pay-to-Public-Key Hash" (P2PKH) wallets.
- Post-quantum spends cost ~$0.01 per UTXO in on-chain transaction fees for typical two-input, single-signature transactions.
- With CashToken-based aggregation, post-quantum sweeps of 400+ unique addresses or 800+ inputs are less than $0.10.
- While SLH‑DSA‑SHA2‑128s (SPHINCS+) signatures weigh in at 7,856 bytes, CashToken-based delegation and Bitcoin Cash's UTXO model allow Quantumroot to use LM-OTS signatures (RFC 8554) – improving quantum security, while also reducing signature sizes (2,180 bytes) and preventing on-chain privacy leaks.
- Quantumroot can support quantum multi-signature (30+ signers), cross-vault signature aggregation (each signature used by multiple, multi-signature vaults), sweep-free vault upgrades and key rotations, threshold and fallback conditions, time-delayed withdrawals, percentage or amount-based pre-authorizations, inheritance and business-continuity configurations, destination-based withdrawal rules, and more.
Most importantly, Quantumroot is quantum safe "at rest" from day 1, even if quantum attackers suddenly steal all Taproot-held BTC. On "Q-Day", Quantumroot wallets can smoothly continue operation – or even reduce their post-quantum transaction sizes by retiring pre-quantum signing.
Following Bitcoin Cash's 2025 upgrade, all Quantumroot components are possible on BCH mainnet – today.
This developer preview combines these components with 10-100× transaction size reductions and code simplifications made possible by several 2026 Cash Improvement Proposals (CHIPs): Loops, Functions, P2S, and Bitwise.
I plan to continue verification, complete security audits of specific Quantumroot CashAssembly templates, and provide an open source implementation for wallets to integrate via Libauth, targeting Bitcoin Cash's May 2026 Upgrade.
Quantumroot: a Quantum-Secure Taproot
Each receiving address of a Quantumroot vault is a Merkle root – a hash which locks the vault using any number of spending conditions. On spend, only one of these conditions is publicly revealed, along with the data required to prove that the spend is valid.
This approach is quite similar to BTC's Taproot address type (BIP341). However, Quantumroot addresses are far more capable than Taproot: CashToken-based delegation, both cross-input and cross-address aggregation, covenant functionality, conditions incorporating messages from external protocols and decentralized financial applications, and of course – low enough network fees for censorship resistance and widespread self-custody.
Is BTC's Taproot quantum secure?
No. Taproot is uniquely quantum vulnerable – even more than typical single-signature wallets (P2PKH).
P2PKH addresses are quantum-vulnerable only at spend time, and could be made quantum-secure (via an upgrade) without moving funds. On the other hand, all Taproot addresses leave a quantum-vulnerable master public key exposed on-chain, enabling long-running attacks and theft by the earliest cryptographically-relevant quantum computers (CRQCs).
Even "script-only" Taproot addresses, which use placeholder "Nothing Up My Sleeve" (NUMS) points to avoid defining a Taproot master key, are vulnerable to these long-range attacks. In fact, such NUMS-based addresses are particularly vulnerable: each NUMS point can be considered a global private key – a quantum NUMS bounty, enabling a single long-range attack to sweep funds from thousands of unrelated "script-only" Taproot addresses.
Can an upgrade make BTC's Taproot quantum secure?
No. Any upgrade which attempts to "shut off" key-based Taproot spends would necessarily confiscate funds from all key-only Taproot addresses.
Note, Taproot addresses were only introduced in 2021, so such a change would exclusively confiscate "young" BTC addresses, almost certainly from users who recently acquired the BTC (at $16K+ USD). This starkly contrasts with long-brewing discussion regarding prevention of quantum theft from dormant pay-to-public-key (P2PK) addresses – nearly all of which (by volume) have not shown signs of life in more than a decade and are apocryphally considered lost.
Faced with imminent quantum catastrophe, it's possible that Bitcoin Core would race to enact a more limited confiscation, e.g. by assuming that most Taproot v1 address users are relying on BIP32 for key derivation. Normal key-based spends could be disabled, and the protocol could instead require a cryptographic or commit-reveal proof that the user knows a BIP32 key corresponding with the vulnerable public key. However, this alternative still confiscates the funds of all non-BIP32-derived addresses.
With a little more coordination, a "Taproot Version 2" upgrade could re-use some Taproot internals (and the "Taproot" brand). For example, a new "TaprootV2" address type could let wallets truly "disable" the master key spending path rather than relying on the NUMS trick. However, this upgrade would still fundamentally change the locking bytecode (from a public key to a Merkle root), requiring a full on-chain migration of all TaprootV1 funds to the new TaprootV2 address format (or the aforementioned confiscation).
Unfortunately, this would also create yet another fracturing of address anonymity sets, which are already strained on BTC by fee-driven UTXO consolidation and the excessive on-chain costs of practical privacy solutions like CashFusion.
How quantum secure is Quantumroot?
Quantumroot offers the best possible security level relevant to bitcoin: 256-bit classical, 128-bit quantum security strength after the speedup from Grover's algorithm, using only SHA256 – so any quantum threat to Quantumroot would necessarily be a threat to mining and overall network consensus – well beyond what post-quantum industry groups consider likely in the next few decades.
Quantumroot Schnorr+LM-OTS Vault
The developer preview published today demonstrates the end-to-end construction of a "starter" Quantumroot vault: a single-signer design allowing 1) pre-quantum Schnorr spends (today's most efficient signatures) and 2) post-quantum Leighton-Micali One-Time Signatures (LM-OTS).
Please note, this wallet template has not yet been reviewed by anyone else. I'm erring on the side of publishing too early, and I'll continue to correct any security issues and make optimizations prior to practical deployments.
Post-Quantum Signatures
While Quantumroot vaults can also implement other post-quantum signature schemes, this developer preview implements an optimal choice for Bitcoin Cash today: Leighton-Micali One-Time Signatures (LM-OTS), the Leighton and Micali adaptation of the original Lamport-Diffie-Winternitz-Merkle one-time signature as specified by RFC 8554 (and recommended by NIST SP 800-208).
Specifically, the preview construction uses CashToken-authenticated spends to delegate the quantum-signature verification to a sibling input. Quantum multi-signature schemes can thus be trivially supported by distributing tokens to additional signers and specifying thresholds, either via additional Quantumroot paths or simple conditionals in the revealed spending path.
Given the 100KB standard TX size limit, 30+ quantum signatures of an unlimited (e.g. merklized) signer set can easily be validated in a single transaction, and an unlimited number of co-signers can be accommodated by continuing the evaluation across two or more deterministic transactions, i.e. an intermediate covenant. At larger thresholds, however, it's likely more economical to switch to on-chain validation of post-quantum STARKs.
Introspection-Based, Cross-Input Aggregation
Both Pre-Quantum and Post-Quantum spending paths demonstrate a new kind of cross-input aggregation made possible by Bitcoin Cash's powerful, UTXO-based virtual machine (CashVM).
While most inputs typically include their own "proof material" – like a signature – to demonstrate that the transaction is authorized to spend the funds, it's possible to carefully design contracts that accept a secondary, indirect proof using transaction introspection operations. E.g. in the demo's Receive Address
:
- For each unique address spent by the transaction, one
Leaf Spend
input serves as the primary proof of authorization – either Quantumroot leaf can be provided, and proof material must also be included to verify theSchnorr Spend (Leaf A)
orToken Spend (Leaf B)
. - For all other UTXOs which precisely match a
Leaf Spend
in one of the other transaction inputs, the fixed-lengthIntrospection Spend
path can be used, rather than unpacking the Quantumroot and verifying the proof material again.Introspection Spends
simply prove that the provided index is a validLeaf Spend
for a matching UTXO. (Note, this requires careful constraint of input bytecode structure, see the contract for details.)
Where P2PKH inputs currently require at least 100 bytes of input bytecode (33-byte encoded public key, minimum 65-byte signature), this demo's Introspection Spends require only 71 bytes (~1 byte leaf spend index, 70-byte P2SH32 redeem bytecode).
By aggregating proof material for all UTXOs with a matching P2SH32 address, the transaction size is reduced by 15% per UTXO, with the overhead of multiple, quantum-ready 32-byte hashes vs. P2PKH's single, 20-byte hash being "paid off" after only 8 UTXOs. (Plausible for e.g. centralized exchange withdrawals and other such non-private, repeated two-party payments today.)
Further, verification requirements are significantly reduced (from N signature checks to one), opening the door to additional future savings. Because the entire remaining 71-byte payload (mostly P2SH32 redeem bytecode) is shared among all matching Introspection Spends
, a potential future upgrade which either safely re-enables non-push unlocking bytecode (e.g. TXv5) and/or otherwise allows input de-duplication (e.g. a deduplication-focused sequence number flag) could eliminate most of the remaining overhead, reducing transaction sizes by an additional 71 bytes per input (and potentially ~35 more, for outpoint hash and sequence numbers).
Privacy Nonces
Beyond basic cross-input aggregation, this vault demonstrates optional deduplication of proof data across multiple unique addresses.
A privacy nonce uniquely hides the Token Spend
path – the CashToken-based aggregated spending path within each Receive Address
to prevent chain analysis from associating active balances with security policies (improving operational security by introducing uncertainty for would-be criminals).
Even when some UTXOs reveal the category of still-in-use tokens, the vault's remaining balance (any unspent UTXOs) – or even its continued existence – cannot be determined without the key, even by quantum attackers.
Note, regardless of the vault details, stronger privacy can be achieved via CashFusion (pre-quantum) or zero-knowledge proof-based covenants (potentially post-quantum) before and after on-chain interactions, isolating long-term storage from inbound or outbound privacy leaks.
CashToken-Based, Cross-Address Aggregation
While the Schnorr Spend
path supports a direct path for small-spend efficiency and to avoid revealing the association between different vault addresses, the Token Spend
path makes this privacy optional – allowing multi-address or even vault-wide sweeps to be authenticated by a single token:
- For the entire vault, an authorized CashToken is held in the
Quantum Lock
address – an unrelated P2SH32 address known only to the vault (onlyReceive Address
es are shared for inbound payments). - To simultaneously spend from many addresses, the CashToken is simply spent to a new address using a
Quantum Unlock
. For the best privacy, vaults should rotate token categories each transaction, taking the new category from a pool created using CashFusion or another privacy solution. Used token categories can be retained or fused to reclaim dust. - Finally, for each unique address spent by the transaction, one
Leaf Spend
input is included, satisfying the contract by simply providing the index at which the authorized token can be inspected. Additional UTXOs of the same address may then use theIntrospection Spend
path pointing to theLeaf Spend
, reducing the unlocking bytecode to 71 bytes (or less, see above).
With cross-address aggregation, the Quantumroot Schnorr+LM-OTS Vault
hugely optimizes transaction sizes for an important moment:
As CRQCs approach, migrations to post-quantum-first vaults, even simply dropping pre-quantum spending conditions, are likely to rise – potentially in news-driven bursts. This could unusually-saturate network throughput, even given Bitcoin Cash’s generously over-provisioned, adjustable block size limit (ABLA).
By designing the Quantumroot Schnorr+LM-OTS Vault
to aggregate quantum sweeps from up to 400+ unique addresses or 800+ inputs in a single <100KB transaction, vault owners are maximally-prepared for the unusual phase change of any “Q-Day” scenarios.
CashToken-Based Delegation
As demonstrated above, CashToken-based delegation – authorizing a wallet-known CashToken category to perform some kind of validation – is a particularly powerful technique. Not only can it significantly reduce transaction sizes and validation costs: it can also simplify very specialized use cases, making them much more practical to implement and audit.
Even quantum multi-signature vaults with complex time, pre-authorization, or delegation-based fallback patterns are byte efficient: zero additional bytes per UTXO, with only the 32-byte privacy nonce per address, 36 bytes per signer per address, and one raw ~2.5KB quantum unlock (plus message randomizer and associated material) per authorization.
In fact, vault sweeps can even be interleaved in a single transaction. E.g. a 1-of-1 sweep can share a signature with a simultaneous 3-of-3 sweep, in cases where related "spending" and "savings" wallets are being upgraded, or for various business vault configurations.
After spending, privacy nonces continue to hide the existence and any remaining balance of either vault, even from the other signers – offering inexpensive practical and operational security. Again, for stronger post-quantum privacy, you'd likely want to minimize leaks from inbound or outbound payments using zero-knowledge BCH covenants, e.g. a covenant which accepts deposits, withdrawals, and internal transactions via on-chain verification of post-quantum STARKs.
Sign up for bitjson's blog
Software security, markets, and bitcoin cash.
No spam. Unsubscribe anytime.
Appendix A: Demo Vault Stats
Below is a snapshot of stats from the preview vault implementation. You can also manually review the sample transactions exported by the test suite.
Baseline Stats
- P2PKH locking bytecode length: 25 bytes
- P2SH32 locking bytecode length: 35 bytes
- Per-input encoding overhead (v1/v2 transactions): ~41 bytes
- P2PKH unlocking bytecode length: 100 bytes (Schnorr) to ~106 bytes (ECDSA)
Post-Quantum Spends
- Quantum signature length (just
Y
): 2,144 bytes - Quantum "public key" (hash) length: 32 bytes
- Quantum unlock (P2SH32): 35 bytes
- Total quantum unlock bytes per-signer (RFC 8554 compatibility overhead + contract): ~2.5KB
- Token Spend unlock length (with Quantumroot unpack): 177 bytes.
- Additional UTXOs unlock length: 71 bytes.
Pre-Quantum Spends
- Quantumroot-wrapped P2PKH unlock: 207 bytes.
- Additional UTXOs unlock length: 71 bytes.
Please let me know if I've missed anything here, or if you identify further optimizations.
Appendix B: On CashAssembly
Before reviewing the code, here are some tips for reading and writing CashAssembly:
- Values are minimally-pushed by
< >
. E.g.<1>
compiles toOP_1
. - Everything is a script. E.g.
<OP_1>
is the minimal push of the bytecode forOP_1
, i.e.<0x51>
(0x0151
). - You can use CashVM during compilation with
$( )
internal evaluations, which return their result (or fail the compilation on failure – useful for assertions). E.g. a P2PKH output is:OP_DUP OP_HASH160 <$( <key.public_key> OP_HASH160 )> OP_EQUALVERIFY OP_CHECKSIG
. - CashAssembly compilers can safely ignore comments (
// single-line
or/* multi-line */
), but special comment syntax is also supported to aid development:- Stack labels: the final
[[ 3rd, 2nd, top ]]
in any comment will apply the labels3rd
,2nd
, andtop
to the stack item values at those depths; labels are aggressively applied in Bitauth IDE to make the evaluation viewer easier to follow during omniscient debugging. - Stack assertions: the first
*[ ... 3rd, 2nd, top | alt: ... 2alt, top ]*
in any comment asserts that the appropriate labels exist for the stack item values at the defined locations; the| alt
segment optionally checks the alternate stack, and each...
is also optional (makes the CashAssembly more portable). (Note 2025-7-1: continuous stack assertion checking isn't released in Bitauth IDE, though stack labels already work.) - It's good practice to either 1) label a new result or 2) assert a stack configuration after most operations, and it's especially important at the boundaries of complex code blocks.
- Stack labels: the final
And here are some things to know or notice below:
- The CashTokens upgrade (2023) made Bitcoin Cash's virtual machine (CashVM) computationally universal. Messages can be passed between contracts using token commitments, see the CashToken Usage Examples for details.
OP_NUM2BIN
andOP_BIN2NUM
allow conversion to and from fixed widths for hash preimages, CashToken commitments, etc. E.g.<1> <4> OP_NUM2BIN
equals<0x01000000>
(note the little-endianness; useOP_REVERSEBYTES
for big-endian, off-chain compatibility). However, note that 1)OP_NUM2BIN
is the only numeric operation which accepts non-minimal numbers, and 2) understand thatOP_NUM2BIN
is optimized for numbers; non-numeric use will surprise you when it's negative, e.g.<0xdeadbeef> <5> OP_NUM2BIN
equals<0xdeadbe6f80>
.- BCH has near-unlimited, big-integer VM numbers (only limited by stack length). However, expensive arithmetic operations (
OP_MUL
,OP_DIV
,OP_MOD
) on very long numbers will quickly fail due to VM Limits. - The BCH VM currently limits stack items at 10KB. (And the modern VM Limits were designed to safely allow further increases, e.g.
1MB
.) Given loops, longer stack items tend to be better for encoding: a2,144
-byte quantum signature only requires 3 bytes of overhead to encode withOP_PUSHDATA2
, but it takes 67 bytes of overhead to push each signature component of aw=4
LM-OTS signature. If you're consuming each in a loop, the<32> OP_SPLIT
only adds a couple bytes to the encoded program. - 2026 CashAssembly (used by Bitauth IDE) supports several 2026 Cash Improvement Proposals (CHIPs): Pay to Script (P2S), Loops, Functions, and Bitwise.
- All loop constructions are built with the indefinite
OP_BEGIN ... OP_UNTIL
.OP_UNTIL
ends the loop on the first truthy value (i.e.OP_BEGIN <0> OP_UNTIL
is an infinite loop, and quickly fails due to VM limits). Note that BCH dodgedMINIMALIF
(enforced on the hijacked BTC network), and truthy successes are very useful for minimizing contract lengths. - Functions are
OP_DEFINE
d, thenOP_INVOKE
d. E.g.< <10> OP_MUL > <13> /* [[ f_10x ]] */ OP_DEFINE
defines a function at function index13
, which can later be invoked by<13> /* *[ ... f_10x ]* */ OP_INVOKE
. - There is no
OP_LSHIFT
orOP_RSHIFT
(these would be footguns). 2026 CashAssembly hasOP_LSHIFTNUM
/OP_RSHIFTNUM
which first assert operation on valid/minimally-encoded numbers
- All loop constructions are built with the indefinite
- On stack scheduling/juggling and program logistics:
<-1>
(OP_1NEGATE
) through<16>
(OP_16
) use just one byte, so prefer small numbers where possible.- Learn how to use
OP_OVER
,OP_TUCK
,OP_IFDUP
, andOP_DEPTH
: these unlock a new level of byte-efficiency for many constructions. - Start optimization attempts with
OP_3DUP
, then try the "OP_2s":OP_2DROP
,OP_2DUP
,OP_2OVER
,OP_2ROT
,OP_2SWAP
. Whenever you can do multiple things with one byte, you're saving a byte later in the program.OP_OVER
andOP_TUCK
count here too – they simultaneously copy and move. - Consider one-off stack operations to be code smells, particularly given two or more in a row: before settling for
OP_DUP
,OP_DROP
,OP_NIP
,OP_ROT
, orOP_SWAP
, try to prove that you can substitute with multi-part operations. OP_PICK
is the greatest code smell – you almost certainly need to rearrange your program and/or unlocking bytecode. Likewise forOP_ROLL
, unless another constraint prevents you from re-arranging (e.g. the Quantumroot receive address).- Understand function overhead: a function generally requires at least 3 extra bytes to define, then 2 per invocation (for the most-encoded 17 functions,
OP_0
throughOP_16
). It makes sense to extract 3-byte instruction patterns when encoded at least 3 times, and 4+ byte patterns when encoded twice. Mind that after refactoring, nested functions can often be profitably re-inlined into the one or two places they're invoked. - At boundaries between code blocks, use a stack-scheduling solver (e.g. Bitcoin Cash VM Stack Wizard) to learn the most efficient patterns to connect them. However, in later optimization passes, don't be afraid to rework one or both blocks together to optimize the handoff beyond edge-level juggling, especially by start at the evaluation's end and working toward the beginning. (Again, aim to use the "OP_2s" as much as possible.)
- Consider modifying your algorithms to loop "backwards" – this is often more efficient in the stack-oriented paradigm because it minimizes stack manipulation: what you need at each step is often already on top (esp. following an
OP_SPLIT
). Looping "forwards" generally only improves efficiency in cases where algorithms somewhat leak their internals. In the below Quantumroot example vault, you'll see both: the checksum computation loops backward, as the aggregation is commutative, while the signing and verification loops are forced to loop forward for RFC 8554 LM-OTS compatibility (the backward construction has less overhead but requires more internal math to interleave the indexes, consuming any savings). A hypothetical BCH-flavored RFC 8554 would be equivalent and save some negligible bytes (dwarfed by the ~2.2KB quantum signatures) during quantum sweeps, but perfect compatibility with RFC 8554 makes this vault more portable and easier to audit.
Appendix C: CashAssembly & Explanations
Below, this article reproduces a selection of the CashAssembly scripts (and explanatory comments) in the developer preview: Quantumroot Schnorr+LM-OTS Vault
.
To follow along, I recommend you simply open the project in Bitauth IDE, where you can hover, click around, and explore what's happening in the omniscient debugger on the right half of the view.
Receive Address
Unlocking Scripts
Introspection Spend
/**
* The Aggregated Spend scenario spends 8 inputs:
* - Input 0: the "master" token spend and quantum signature (Address Q)
* - Input 1: a leaf spend of Address A
* - Input 2: a leaf spend of Address B
* - Inputs 3 and 4: introspection spends of Address A
* - Inputs 5 and 6: introspection spends of Address B
* - Input 7: introspection spend of Address Q
*
* This particular unlock is used by Input 3: an introspection spend of
* Address A. To do so requires pointing at input 1, a valid leaf spend
* of Address A.
*/
<leaf_spend_index>
Reject: Bypass Attempt
/**
* This unlock demonstrates that the locking script isn't vulnerable
* to a potential bypass: in addition to checking that a matching
* sibling input exists, we need to check that it's being validated
* via a "leaf" spending path. Otherwise, 2+ such UTXOs may be spent
* via the introspection path by only pointing at each other, with no
* "real" proof provided in any input.
*
* The "Paired Bypass Attempt" scenario tries this: the transaction
* includes two inputs which each attempts to point to the other to
* prove an introspection spend.
*/
<leaf_spend_index>
Schnorr Spend (Leaf A)
/**
* "Pre-Quantum Aggregated Spend" includes 20 inputs:
*
* - Input 0: a schnorr spend of Address A
* - Input 1 through 19: introspection spends of Address A
*
* This unlock is used by Input 0: a leaf spend of
* Address A using the Schnorr path.
*/
OP_1NEGATE // -1 proves (to sibling inputs) that this is a leaf spend
schnorr_unlock
<0> // 0 for leaf A, 1 for leaf B [[ leaf_index ]]
<$(<receive_address_token_spend> OP_HASH256)> // other leaf hash: [[ sibling_leaf_hash ]]
<receive_address_schnorr_spend> // [[ this_leaf ]]
Token Spend (Leaf B)
OP_1NEGATE // -1 proves this is a leaf spend to sibling inputs
token_unlock
<1> // 0 for leaf A, 1 for leaf B [[ use_leaf_B ]]
<$(<receive_address_schnorr_spend> OP_HASH256)> // other leaf hash: [[ sibling_leaf_hash ]]
<receive_address_token_spend>
Receive Address
Locking Script
/**
* - Introspection Spend: accepts a number <N>, i.e. "check
* input index N for a matching leaf spend". The contract then
* enforces that only leaf spends may be prefixed with OP_1NEGATE
* (negative one), such that the prefix proves its a leaf spend.
*
* - Leaf Spend: every transaction must have at least one leaf spend.
* The leaf spend branch accepts a script to execute, the data needed
* to prove that the script is part of the quantumroot hash, and
* any additional data the executed script needs.
*
* Note that we could save some bytes by allowing some malleability,
* but this implementation considers a few bytes per input to be
* reasonable for minimizing unexpected behavior in the short term.
*
* In the longer term, an upgrade like TXv5 would allow wallets to
* trim out the malleability protection without enabling malleability
* (detached data/signatures comprehensively prevent malleability).
*
* Additionally, non-push unlocking bytecode would allow aggregated
* spends to introspect and evaluate the entire INPUTBYTECODE of a
* matching aggregated spend, trimming their length to just a few
* bytes, esp. by deduplicating the redeem bytecode. (The aggregated
* spending path would remain useful: operation cost budget is
* allocated based in input bytecode length, so a fast/efficient path
* is more likely to weigh-in under the VM limits than naively
* re-executing expensive proofs.)
*/
OP_DEPTH
OP_1SUB
OP_IFDUP
OP_IF // CashVM's OP_IF accepts any non-zero value, so leaf spends can use the whole stack
/**
* Attempting leaf spend: verify that the pushed bytecode
* and sibling leaf match the quantumroot, then execute.
*/
OP_ROLL // [[ leaf_spend_indicator ]] should be -1
OP_NEGATE // [[ should_be_1 ]] validated by negating + defining, then invoking 1
OP_2DUP // *[ ... this_leaf, should_be_1 ]*
OP_DEFINE // Not evaluating yet, just saving as a function.
OP_DROP // No need to check, we verify by invoking <1>. (And OP_VERIFY wouldn't prevent malleability.)
OP_HASH256 // [[ this_leaf_hash ]]
OP_ROT // *[sibling_leaf_hash, this_leaf, this_leaf_is_A]*
OP_DUP OP_SIZE OP_EQUALVERIFY // Delete these 3 bytes if leaf-spend malleability is acceptable or otherwise prevented
OP_NOTIF
OP_SWAP
OP_ENDIF
OP_CAT
OP_HASH256
<$(
<receive_address_schnorr_spend> OP_HASH256 // [[ leaf_A_hash ]]
<receive_address_token_spend> OP_HASH256 // [[ leaf_B_hash ]]
OP_CAT OP_HASH256
)> // [[ quantumroot ]]
OP_EQUALVERIFY // Confirmed match, just execute:
<1> // *[ should_be_1 ]*
OP_INVOKE
OP_ELSE
/**
* Attempting introspection spend: verify that the indicated
* input contains a matching leaf spend.
*/
OP_DUP
/**
* Note that by directly using leaf_spend_index as an introspection
* index, we implicitly prevent introspection spends from 1) numeric
* malleability, and 2) having OP_1NEGATE as a first byte. Further,
* because we're not using a valid input index (like `0` or `1`),
* introspection spends are allowed to point to a matching leaf spend
* in practically any input.
*/
OP_UTXOBYTECODE
OP_INPUTINDEX OP_UTXOBYTECODE OP_EQUALVERIFY
OP_INPUTBYTECODE // [-[ input_of_claimed_leaf_spend ]]
<1> OP_SPLIT OP_DROP // Only leaf spends may be prefixed with OP_1NEGATE.
<OP_1NEGATE> OP_EQUAL // If successful, proves a matching leaf spend.
OP_ENDIF
Receive Address: Schnorr Spend
Scripts
Unlock
<key.schnorr_signature.default>
Lock
/**
* The single-signature spending path, hashed into the
* first leaf of this vault's "quantumroot".
*
* root
* / \
* You are here. --> A B
*
* Note that unlike token spends, no "privacy nonce" is
* necessary for Schnorr-based spends, as the public key
* already serves as a nonce (hardened HD keys cannot be
* associated, even by quantum adversaries.)
*
* See "Receive Address: Token Spend" for details.
*/
<key.public_key>
OP_CHECKSIG
Receive Address: Token Spend
Scripts
Unlock
<token_spend_index>
Lock
/**
* The token-based spending path, hashed into the second leaf of this
* vault's "quantumroot":
*
* root
* / \
* A B <-- You are here.
*
* Note that this Schnorr + LM-OTS Vault only uses two leaves, but any
* kind of tree can be encoded in the quantumroot, e.g. a 3-leaf tree
* could accept another sibling hash at this level and execute either
* committed script:
*
* root
* / \
* A (BC)
* / \
* B C
*/
/**
* This nonce prevents chain analyzers from exhaustively iterating over
* all CashTokens in the UTXO set to identify connections between
* Schnorr-spent Quantumroot outputs and vault NFTs. The connection
* is only revealed for the address(es) swept in a quantum spend (even
* for quantum adversaries).
*
* Note that the nonce isn't revealed – and therefore does not increase
* transaction sizes – unless the UTXO is being spent via a cross-address
* CashToken spend; in this vault, the quantum spend.
*
* Even during CashToken-based spends, other UTXOs of the same address do
* not have to duplicate this nonce across additional inputs: the nonce
* only needs to be provided for one leaf-spent input, and all other UTXOs
* with a matching address can use the leaner introspection-based spend.
*/
<$(<nonce_source.public_key> OP_HASH256 )> // [[ address_privacy_nonce ]]
OP_DROP
/**
* Here's where the actual validation begins: check that the provided
* input index has the expected token category (to which we delegated
* authorization). If the transaction proves it can spend that token,
* we're done here.
*/
OP_UTXOTOKENCATEGORY
<vault_token_category>
OP_EQUAL
/**
* Note how trivially we can construct more advanced schemes: quantum
* multi-signature or time-based, pre-authorization, or
* delegation-based fallbacks, etc.
*
* For example, here's how we'd bump this vault up to a
* 3-of-3 quantum multi-signature (36 bytes per signer):
*
* OP_UTXOTOKENCATEGORY <vault_token_category2> OP_EQUALVERIFY
* OP_UTXOTOKENCATEGORY <vault_token_category3> OP_EQUALVERIFY
*
* Efficient aggregation remains supported (sweeping from any number
* of addresses), and it's even possible to sweep fund from multiple
* kinds of unrelated vaults using a single signature.
*
* For example, a single transaction could aggregate sweeps from both
* a single-signature quantum vault and 3-of-3 quantum vault, where
* the single-signer is one of the three 3-of-3 signers. (While this
* leaks the relationship at spend time, differing privacy nonces
* could safely hide the relationship "at rest" – even from the other
* two 3-of-3 signers, and even if equipped with a cryptographically
* relevant quantum computer; they'd discover the connection only
* if/when the single-signer includes it in a transaction. Such
* at-rest privacy is highly valuable for the operational security of
* vaulting systems.
*/
Quantum Lock: Serialize Transaction
Script
/**
* Script used to construct the commitment to the current
* transaction, preventing malleation in the mempool
* (even by a fast quantum attacker).
*
* This particular demo produces a concatenation of:
* - **The spent outpoints**: for better air-gapped support
* (essentially a partial SIGHASH_UTXOs to ensure that an
* offline wallet doesn’t need any further chain state to
* double-check the transaction it’s signing.)
* - **The outputs**: to prevent any modifications to the
* outputs by an attacker.
*
* Note that this script could save ~30 bytes with some
* sort of: `<all_outputs_all_utxos> OP_SIGHASH`, but more
* research is required. A more general "detached signature"
* structure (and matching introspection opcode)
* might be more widely useful – saving more bytes in more
* cases, as well as simplifying malleability prevention.
*/
OP_TXINPUTCOUNT // Lock down the count, too.
OP_TXINPUTCOUNT
OP_BEGIN
OP_1SUB // the next index to aggregate
OP_DUP
OP_OUTPOINTTXHASH
OP_OVER
/**
* Could probably trim off the padding here and rely on the
* second-preimage resistance of OP_HASH256, but would
* technically imply less than 128-bit quantum security given
* Grover's algorithm, so we do the safe thing and just pad it.
*/
OP_OUTPOINTINDEX <2> OP_NUM2BIN
OP_CAT
/**
* This efficient concatenation approach happens to leave items in
* reverse order, with OP_TXINPUTCOUNT at the end. Either way
* is fine, we just want to minimize on-chain validation bytes.
*/
OP_ROT OP_CAT
OP_SWAP
OP_IFDUP
OP_NOT
OP_UNTIL
/**
* Locking down the outpoint hashes here offers some defense in depth
* (esp. if the output digest approach below is ever modified).
*/
OP_SHA256 // [[ hash_outpoints ]]
OP_TXOUTPUTCOUNT
OP_TXOUTPUTCOUNT
OP_BEGIN
OP_1SUB // the next index to aggregate
OP_DUP
OP_2DUP
OP_2DUP
/**
* Pre-hashing the most variable-length fields is ideal in that
* it both ensures a fixed width in the final commitment
* (preventing window-sliding malleation) and eliminates any variance
* in the number of outputs which can be concatenate in a single stack
* item, improving predictability and support for unusual output types.
*/
OP_OUTPUTBYTECODE OP_SHA256
OP_ROT
OP_OUTPUTVALUE <8> OP_NUM2BIN
OP_ROT
/**
* Particularly important to fix the width, can vary between 0 and
* 33 bytes.
*/
OP_OUTPUTTOKENCATEGORY <33> OP_NUM2BIN
/**
* Concatenate what we have so far.
*/
OP_CAT OP_CAT
OP_ROT
OP_OUTPUTTOKENAMOUNT <8> OP_NUM2BIN
OP_ROT
/**
* Again, pre-hashing improves predictability and support.
*/
OP_OUTPUTTOKENCOMMITMENT OP_SHA256
OP_CAT OP_CAT
/**
* Concatenated all details for this output, now append it
* to the last round's concatenation (again, reverse order
* is a bit more efficient here).
*/
OP_ROT OP_CAT
OP_SWAP
OP_IFDUP
OP_NOT
OP_UNTIL
OP_SHA256 // [[ hash_outputs ]]
OP_CAT OP_SHA256 // [[ serialization_hash ]]
Quantum Lock: Verify Transaction Shape
Script
/**
* No additional unlocking bytecode is necessary.
*
* The locking bytecode uses introspection to produce
* a signing serialization for the transaction, hashes
* it, then checks that the hash matches the one expected
* by the signing wallet.
*
* Critically, direct introspection opcodes ensure
* post-quantum security by allowing the transaction's
* "preimage" to be inspected without reliance on
* elliptic-curve cryptography as would be required
* by the "OP_CHECKSIG + OP_CHECKDATASIG trick".
*
* As a result, any element of the transaction's shape
* which is inspected and verified by this locking script
* **cannot be malleated**, even by fast quantum attackers,
* while the transaction is waiting to be mined in a block.
*/
quantum_lock_serialize_transaction
OP_HASH256
<$(
quantum_lock_serialize_transaction
OP_HASH256
)>
OP_EQUAL
Quantum Lock
Unlocking Scripts
Introspection Unlock
<quantum_spend_index>
Quantum Unlock
OP_1NEGATE // -1 proves (to sibling inputs) that this is a leaf spend
<$(quantum_signature)> // [[ quantum_signature ]]
<$(quantum_key_identifier)> // [[ I ]]
<0> // [[ q ]] is always 0 for privacy (retained for RFC compatibility)
<$(quantum_message_randomizer)> // [[ C ]]
<$(quantum_signed_message)> // [[ quantum_signed_message ]]
Quantum Lock
/**
* An implementation of Leighton-Micali One-Time Signatures
* (LM-OTS), the Leighton and Micali adaptation of the original
* Lamport-Diffie-Winternitz-Merkle one-time signature as
* specified by RFC 8554 and recommended by NIST SP 800-208.
*
* Note that the quantum signature simply signs the hash of a
* script to be executed if the signature can be verified. This
* allows the transaction's "signing serialization algorithm"
* to be upgraded (by the wallet) without moving funds.
*
* The signed script includes both the instruction for checking
* the transaction and commitment(s) to the expected values.
*
* This approach maximizes the vault's flexibility: vault UTXOs
* which have been dormant for decades can be offline-upgraded
* to employ the latest VM bytecode operations in verifying the
* signed transaction's shape. If new kinds of primitives
* (e.g. CashTokens) or more efficient serialization opcodes
* (e.g. OP_DETACHEDSIGNATURE) are introduced, wallets can be
* seamlessly-upgraded to use them without moving any UTXOs.
*
* Note this quantum lock also allows itself to be swept via
* an introspection spend: if the address somehow accumulates
* multiple UTXOs (i.e. dusting or unsolicited airdrops), they
* can all be cheaply spent at the same time (just one quantum
* signature), and the user doesn't have to sweep or reveal the
* connection to any of the vault's real receive addresses (which
* themselves support aggregation, too).
*/
OP_DEPTH
OP_1SUB
OP_IFDUP
OP_IF
/**
* Attempting leaf spend: verify that the pushed bytecode
* and sibling leaf match the quantumroot, then execute.
*/
OP_ROLL // [[ leaf_spend_indicator ]] should be -1
OP_NEGATE // [[ should_be_1 ]] validated by negating + defining, then invoking 1
OP_2DUP // *[ ... quantum_signed_message, should_be_1 ]*
OP_DEFINE // Not evaluating yet, just saving as a function.
OP_DROP // No need to check, we verify by invoking <1>. (And OP_VERIFY wouldn't prevent malleability.)
/**
* Perform a quantum "CHECKDATASIG":
*/
lm-ots_derive_key_candidate // Fails on invalid quantum_signature
<$(quantum_public_key)> // [[quantum_public_key]]
OP_EQUALVERIFY
/**
* The quantum signature was valid, evaluate the signed message:
* a simple script to inspect the transaction and verify it
* hasn't been tampered with prior to inclusion in a block.
*/
<1> // *[ should_be_1 ]*
OP_INVOKE
OP_ELSE
/**
* Attempting introspection spend: verify that the indicated
* input contains a matching quantum spend.
*/
OP_DUP
/**
* Note that by directly using leaf_spend_index as an introspection
* index, we implicitly prevent introspection spends from 1) numeric
* malleability, and 2) having OP_1NEGATE as a first byte. Further,
* because we're not using a valid input index (like `0` or `1`),
* introspection spends are allowed to point to a matching quantum
* spend in practically any input.
*/
OP_UTXOBYTECODE
OP_INPUTINDEX OP_UTXOBYTECODE OP_EQUALVERIFY
OP_INPUTBYTECODE // [-[ input_of_claimed_leaf_spend ]]
<1> OP_SPLIT OP_DROP // Only leaf spends may be prefixed with OP_1NEGATE.
<OP_1NEGATE> OP_EQUAL // If successful, proves a matching leaf spend.
OP_ENDIF
LM-OTS (RFC 8554) CashAssembly Implementation
LM-OTS Checksum
/**
* Compute checksum for w=4 (fixed; n=32, u=64, v=3, p=67, ls=4).
*
* This sub-script is used both offline and on-chain, so
* byte-length optimization is critical.
*
* Note that this approach is optimized for w=4 in CashAssembly vs.
* the parameterized C-style implementation in RFC 8554. Looping
* backwards saves a byte or two, the coefficient extraction
* ("coef") function is inlined, and all constants are baked in.
*
* (This sub-script is also tested with fast-check in the repo.)
*/
OP_SIZE // Could be "<32>", but OP_SIZE saves a byte.
<> // [[ sum_slot ]]
OP_TOALTSTACK
OP_BEGIN
OP_1SUB
OP_SWAP
OP_OVER
OP_SPLIT // [[ last_byte ]]
<30>
OP_OVER
<4> OP_RSHIFTBIN OP_BIN2NUM // [[ upper_nibble ]]
OP_SUB
OP_SWAP
<0x0f> OP_AND OP_BIN2NUM // [[ lower_nibble ]]
OP_SUB
OP_FROMALTSTACK
OP_ADD
OP_TOALTSTACK
OP_SWAP
OP_IFDUP
OP_NOT
OP_UNTIL OP_DROP
OP_FROMALTSTACK
<4> OP_LSHIFTNUM
LM-OTS Derive Key Candidate
/**
* An implementation of Leighton-Micali One-Time Signatures
* (LM-OTS), the Leighton and Micali adaptation of the original
* Lamport-Diffie-Winternitz-Merkle one-time signature as
* specified by RFC 8554 and recommended by NIST SP 800-208.
*
* This script derives a key candidate from an LM-OTS signature.
* It's evaluated on chain, so byte-length optimization is critical.
*
* Note that this approach is optimized for w=4 in CashAssembly vs.
* the parameterized C-style implementation in RFC 8554. The
* coefficient extraction ("coef") function is inlined, and all
* constants are baked in.
*
* (This sub-script is also tested with fast-check in the repo.)
*
* Expected stack: *[ Y, I, q, C, message ]*
*/
OP_TOALTSTACK
OP_SIZE <32> OP_EQUALVERIFY // Require a valid message randomizer
OP_TOALTSTACK
<4> OP_NUM2BIN // [[padded_q]]
OP_CAT // Prepared prefix: [[ I_and_q ]]
OP_DUP
<0x8181> // [[ D_MESG ]]
OP_FROMALTSTACK
OP_FROMALTSTACK
OP_CAT OP_CAT OP_CAT // [[ message_preimage ]]
OP_SHA256 // the message hash to be signed: [[ Q ]]
OP_DUP
lm-ots_checksum // [[ checksum ]]
<
<2> OP_NUM2BIN OP_REVERSEBYTES // (big-endian for RFC 8554)
OP_CAT
> <0x0c> // [[ u16str_cat, f_u16str_cat ]]
OP_DEFINE
<0x0c> OP_INVOKE // [[ encoded_message_hash ]]
OP_TOALTSTACK
OP_SWAP
OP_TOALTSTACK
<
/**
* Input: *[ ... I_and_q, i, j | alt: ... step_0 ]*
* Output: *[ ... | alt: ... sig_part ]*
*/
OP_DUP
<15> OP_NUMNOTEQUAL
OP_IF
OP_TOALTSTACK
OP_2DUP
<0x0c>
OP_INVOKE // [[ I_through_i_upper ]]
OP_FROMALTSTACK // *[... j ]
<1> OP_NUM2BIN // 0 must be padded for RFC 8554 compatibility
OP_BEGIN // *[ ... I_through_i_upper, j | alt: ... step_0 ]*
OP_2DUP OP_CAT // [[ I_through_j ]]
OP_FROMALTSTACK
OP_CAT // [[ next_step_preimage ]]
OP_SHA256 // [[ next_step ]]
OP_TOALTSTACK
// OP_SWAP // *[ ... j_end, j ]*
OP_BIN2NUM OP_1ADD // [[ next_j ]] (accepts padded 0 for RFC 8554 compatibility)
OP_DUP
<15>
OP_GREATERTHANOREQUAL
OP_UNTIL // *[ ... | alt: ... upper_sig_part ]*
OP_2DROP
OP_ELSE
OP_DROP
OP_ENDIF
> <0x0d> // [[ chain_step, f_chain_step ]]
OP_DEFINE
<> // [[ signature_slot ]]
OP_SWAP
<0> // i (wallet-only bytecode, looping forward for simplicity)
/**
* Loop input: *[ signature_slot, I_and_q, i | alt: encoded_message_hash, x ]*
*/
OP_BEGIN
OP_FROMALTSTACK
OP_FROMALTSTACK
<1> OP_SPLIT // [[ message_byte, remaining_message_hash ]]
OP_SIZE // [[ has_more_bytes ]]
OP_SWAP // *[ ... has_more_bytes, remaining_message_hash ]*
OP_TOALTSTACK OP_TOALTSTACK
OP_SWAP // *[ x ]*
<32> OP_SPLIT // [[ upper_step_init, remaining_x ]]
OP_TOALTSTACK // *[ ... message_byte, upper_step_init | alt: remaining_message_hash, has_more_bytes, remaining_x ]*
OP_SWAP
OP_DUP OP_TOALTSTACK // Save message_byte for lower nibble
<4> OP_RSHIFTBIN OP_BIN2NUM // [[ upper_j ]]
OP_SWAP // *[ ... upper_step_init ]*
OP_TOALTSTACK
/**
* Input: *[ signature_slot, I_and_q, i, upper_j | alt: ... upper_step_init ]*
*/
<0x0d>
OP_INVOKE // *[ signature_slot, I_and_q, i | alt: ... upper_sig_part ]*
OP_1ADD // [[ i_lower ]]
OP_FROMALTSTACK // [[ upper_sig_part ]]
OP_ROT
OP_ROT
OP_FROMALTSTACK // *[ ... message_byte ]*
<0x0f> OP_AND OP_BIN2NUM // [[ lower_j ]]
OP_FROMALTSTACK
OP_FROMALTSTACK // Top: *[ signature_slot, upper_sig_part, I_and_q, i_lower, lower_j, remaining_x, has_more_bytes | alt: remaining_message_hash ]*
OP_IF
<32> OP_SPLIT // [[ lower_step_init, remaining_x ]]
OP_TOALTSTACK OP_TOALTSTACK
/**
* Input: *[ ... I_and_q, i_lower, lower_j | alt: ... lower_step_init ]*
*/
<0x0d>
OP_INVOKE // *[ signature_slot, upper_sig_part, I_and_q, i_lower | alt: ... lower_sig_part ]*
OP_2SWAP
OP_FROMALTSTACK // [[ lower_sig_part ]]
OP_CAT // [[ both_sig_parts ]]
OP_CAT // [[ signature_so_far ]]
OP_ROT OP_ROT // *[ signature_so_far, I_and_q, i_lower ]*
OP_1ADD // [[ next_i_upper ]]
<0> // [[ continue_loop ]]
OP_ELSE
OP_FROMALTSTACK // By design: don't pollute alt (even if it works for most use cases and saves a byte)
// Remaining_message_hash and lower_j_end are now verified 0, but remaining_x could be longer due to malleation:
// *[ signature_slot, upper_sig_part, I_and_q, i_lower, lower_j, remaining_x, remaining_message_hash ]*
OP_CAT
OP_CAT // If remaining_x was 0, this [[ should_be_zero ]]
OP_NOT // Assert that should_be_zero is 0 (consumed by OP_UNTIL)
OP_ENDIF
OP_UNTIL // *[ signature_slot, upper_sig_part, I_and_q, i_lower ]*
OP_DROP
<0x8080> // [[ D_PBLC ]]
OP_2SWAP
OP_CAT // [[ quantum_public_key_Y ]]
OP_CAT OP_CAT // [[ quantum_public_key_preimage ]]
OP_SHA256 // [[ quantum_public_key ]]
LM-OTS Key Identifier
/**
* RFC 8554 Key Identifier, `I`.
*
* For privacy and bytecode minimization, this vault never re-uses
* seeds or public keys. However, RFC 8554 expects `I` to be
* "chosen uniformly at random, or via a pseudorandom process";
* some bytes could possibly be saved by treating `I` and `q`
* as one empty 20-byte value (or omitting them entirely), but
* that would deviate from the simplest-to-review and most
* widely-reviewed option: setting `I` as required by RFC 8554.
*/
<identifier_source.public_key> OP_HASH256 <16> OP_SPLIT OP_DROP
LM-OTS Message Randomizer
/**
* RFC 8554 requires a random or pseudorandom message
* randomizer ("C") to prevent various kinds of attacks:
* multi-target pre-computation, cross parameter-set
* prefix attacks, catastrophic leaks from accidental
* key reuse or fault attacks, etc.
*
* We use pseudorandom material for determinism, so it's
* critical that we mix-in the signed message itself to
* maximize protection in the unusual event of key reuse.
*
* Note: architecturally, each quantum UTXO is one-time
* use, with little potential for accidental re-use, even
* across many wallet devices and many inbound payments.
*
* This is a feature of CashToken aggregation/delegation:
* receive addresses don't even leak that you're using
* Quantumroot, and unless your wallet makes a quantum
* spend, no receive addresses can be connected with the
* delegated token category (the unrevealed category is
* also hashed with a per-address privacy nonce in the
* hidden quantum spending path).
*
* Since each wallet device can control its own CashToken
* "master key", and wallets always move the token to
* the next address index during a quantum spend, even
* if an attacker is able to make a wallet device forget
* about a past spend, the past spend will typically
* already be confirmed in a block; there's nothing
* left to protect.
*
* However, there may be cases where an attacker is able
* to intercept many spend attempts or otherwise interfere
* more directly with the wallet – in these cases, careful
* message randomizer construction adds additional security.
*/
<nonce_source.public_key> // [[ deterministic_nonce ]]
<$(quantum_lock_serialize_transaction)> // [[ serialization_hash ]]
/**
* This is already RFC 8554 compliant, but NIST SP 800‐208
* requires an approved Deterministic Random Bit Generator
* (DRBG). To modify this scheme for that FIPs requirement
* (i.e. you're building this into a "hardware cryptographic
* module"), drop in an approved DRBG here.
*
* As of now, we're correctly "re-seeding" a double SHA-256
* function (with >256 bits) before every single spend.
*/
OP_CAT OP_HASH256 // [[ message_randomizer ]]
LM-OTS Private Key
/**
* Generate the LM-OTS private key according to the
* key-stretching procedure in RFC 8554.
*
* Inputs: <I> <q> <seed>
* Outputs: <private_key>
*
* Note that this script is only used internally by the
* compiler to stretch a hardened HD key for quantum use,
* it is not evaluated on-chain. If needed, the computation
* density limits in this IDE can be increased by switching
* to the BCH_SPEC VM (top right window corner).
*
* Expected stack: *[ I, q, seed ]*
*/
OP_TOALTSTACK
<4> OP_NUM2BIN // [[padded_q]]
OP_TOALTSTACK
OP_TOALTSTACK
<> // Start accumulated key with an empty stack item.
<0> // [[ initial_i ]]
OP_BEGIN
/**
* Lazy stack scheduling; doesn't need to be efficient,
* this is only evaluated in wallets.
*/
OP_FROMALTSTACK OP_FROMALTSTACK OP_FROMALTSTACK
OP_3DUP OP_TOALTSTACK OP_TOALTSTACK OP_TOALTSTACK
OP_TOALTSTACK OP_CAT // [[ I_and_q ]]
/**
* Little-endian would be more natural in BCH VM, but for
* RFC 8554 compatibility, we reverse:
*/
OP_OVER <2> OP_NUM2BIN OP_REVERSEBYTES // [[ padded_i ]]
<0xff>
OP_FROMALTSTACK
OP_CAT OP_CAT OP_CAT // [[ element_preimage ]]
OP_SHA256
OP_ROT
OP_SWAP
OP_CAT // [[ accumulated_so_far ]]
OP_SWAP
OP_1ADD // [[ next_i ]]
OP_DUP
<67> // [[ required_hash_chains ]]
OP_GREATERTHANOREQUAL
OP_UNTIL OP_DROP // [[ quantum_private_key ]]
LM-OTS Public Key
/**
* Expected stack: *[ x, I, q ]*
*/
<4> OP_NUM2BIN // [[padded_q]]
OP_CAT // Prepared prefix: [[ I_and_q ]]
OP_SWAP
OP_TOALTSTACK
<> // [[ out_slot ]]
OP_SWAP
<0> // [[ i ]]
/**
* Input stack: <out_slot> <I_and_q> <i> | altstack: <quantum_private_key_x>
*/
OP_BEGIN
/**
* Each iteration, slice off the next 32-byte chunk for processing:
*/
OP_FROMALTSTACK <32> OP_SPLIT // [[ remaining_x ]]
OP_TOALTSTACK // [[ next_chunk_x ]]
OP_TOALTSTACK
OP_2DUP
<2> OP_NUM2BIN OP_REVERSEBYTES // [[ padded_i ]] (big-endian for RFC 8554)
OP_CAT // [[ I_through_i ]]
<0x00> // [[ j ]] start with padded 0 for RFC 8554 compatibility
/**
* Input stack: <I_through_i> <j> | altstack: <remaining_x> <chunk>
*/
OP_BEGIN
OP_2DUP OP_CAT // [[ I_through_j ]]
OP_FROMALTSTACK
OP_CAT // [[ next_step_preimage ]]
OP_SHA256 // [[ next_step_chunk ]]
OP_TOALTSTACK
OP_BIN2NUM OP_1ADD // BIN2NUM required since we padded 0 (RFC 8554 compatibility)
OP_DUP
<15> OP_GREATERTHANOREQUAL
OP_UNTIL OP_2DROP
OP_ROT // top: out_slot
OP_FROMALTSTACK // [[ public_key_element ]]
OP_CAT // [[ public_key_so_far ]]
OP_ROT OP_ROT
OP_1ADD
OP_DUP
<67> OP_GREATERTHANOREQUAL
OP_UNTIL
OP_DROP
<0x8080> // [[ D_PBLC ]] quantum_public_key
OP_ROT // [[ quantum_public_key_Y ]]
OP_CAT OP_CAT // [[ quantum_public_key_preimage ]]
OP_SHA256 // [[ quantum_public_key ]]
LM-OTS Sign
/**
* This script generates an LM-OTS signature – it's only evaluated
* inside the wallet during transaction signing.
*
* Signing time only depends on the message, so the algorithm
* is fundamentally resistant to side-channel private key leaks.
* To make signing completely constant-time (i.e. to prevent a
* powerful eavesdropper from correlating the wallet with the
* messages being signed using signing times) simply verify each
* signature before broadcasting it: just verify the transaction,
* e.g. with Libauth's `vm.verify`.
*
* Note that verifying transactions before broadcast is already
* a defense-in-depth best practice. Wallet software faults
* which go undetected all the way to an attempted broadcast of
* an invalid transaction can often jeopardize security and/or
* privacy. This is especially true for one-time signature
* schemes: with only two signature attempts, a listening
* attacker can extract enough hash chain points to forge
* new signatures.
*
* Pre-broadcast validation eliminates both of these risks.
*
* Expected stack: *[ x, I, q, C, message ]*
*/
OP_TOALTSTACK
OP_SIZE <32> OP_EQUALVERIFY // Sanity-check message randomizer
OP_TOALTSTACK
<4> OP_NUM2BIN // [[padded_q]]
OP_CAT // Prepared prefix: [[ I_and_q ]]
OP_DUP
<0x8181> // [[ D_MESG ]]
OP_FROMALTSTACK
OP_FROMALTSTACK
OP_CAT OP_CAT OP_CAT // [[ message_preimage ]]
OP_SHA256 // the message hash to be signed: [[ Q ]]
OP_DUP
lm-ots_checksum // [[ checksum ]]
<
<2> OP_NUM2BIN OP_REVERSEBYTES // (big-endian for RFC 8554)
OP_CAT
> <0x0c> // [[ u16str_cat, f_u16str_cat ]]
OP_DEFINE
<0x0c> OP_INVOKE // [[ encoded_message_hash ]]
OP_TOALTSTACK
OP_SWAP
OP_TOALTSTACK
<
/**
* Input: *[ ... I_and_q, i, j_end | alt: ... step_0 ]*
* Output: *[ ... | alt: ... sig_part ]*
*/
OP_IFDUP
OP_IF
OP_TOALTSTACK
OP_2DUP
<0x0c>
OP_INVOKE // [[ I_through_i_upper ]]
OP_FROMALTSTACK
OP_SWAP
<0x00> // [[ j ]] start with padded 0 for RFC 8554 compatibility
OP_BEGIN // *[ ... j_end, I_through_i_upper, j | alt: ... step_0 ]*
OP_3DUP OP_CAT // [[ I_through_j ]]
OP_FROMALTSTACK
OP_CAT // [[ next_step_preimage ]]
OP_SHA256 // [[ next_step ]]
OP_TOALTSTACK
OP_SWAP // *[ ... j_end, j ]*
OP_BIN2NUM OP_1ADD // [[ next_j ]] (accepts padded 0 for RFC 8554 compatibility)
OP_DUP
OP_ROT
OP_GREATERTHANOREQUAL
OP_UNTIL // *[ ... | alt: ... upper_sig_part ]*
OP_2DROP
OP_DROP
OP_ENDIF
> <0x0d> // [[ chain_step, f_chain_step ]]
OP_DEFINE
<> // [[ signature_slot ]]
OP_SWAP
<0> // i (wallet-only bytecode, looping forward for simplicity)
/**
* Loop input: *[ signature_slot, I_and_q, i | alt: encoded_message_hash, x ]*
*/
OP_BEGIN
OP_FROMALTSTACK
OP_FROMALTSTACK
<1> OP_SPLIT // [[ message_byte, remaining_message_hash ]]
OP_SIZE // [[ has_more_bytes ]]
OP_SWAP // *[ ... has_more_bytes, remaining_message_hash ]*
OP_TOALTSTACK OP_TOALTSTACK
OP_SWAP // *[ x ]*
<32> OP_SPLIT // [[ upper_step_init, remaining_x ]]
OP_TOALTSTACK // *[ ... message_byte, upper_step_init | alt: remaining_message_hash, has_more_bytes, remaining_x ]*
OP_SWAP
OP_DUP OP_TOALTSTACK // Save message_byte for lower nibble
<4> OP_RSHIFTBIN OP_BIN2NUM // [[ upper_j_end ]]
OP_SWAP // *[ ... upper_step_init ]*
OP_TOALTSTACK
/**
* Input: *[ ... I_and_q, i, upper_j_end | alt: ... upper_step_init ]*
*/
<0x0d>
OP_INVOKE // *[ signature_slot, I_and_q, i | alt: ... upper_sig_part ]*
OP_1ADD // [[ i_lower ]]
OP_FROMALTSTACK // [[ upper_sig_part ]]
OP_ROT
OP_ROT
OP_FROMALTSTACK // *[ ... message_byte ]*
<0x0f> OP_AND OP_BIN2NUM // [[ lower_j_end ]]
OP_FROMALTSTACK
OP_FROMALTSTACK // Top: *[ signature_slot, upper_sig_part, I_and_q, i_lower, lower_j_end, remaining_x, has_more_bytes | alt: remaining_message_hash ]*
OP_IF
<32> OP_SPLIT // [[ lower_step_init, remaining_x ]]
OP_TOALTSTACK OP_TOALTSTACK
/**
* Input: *[ ... I_and_q, i_lower, lower_j_end | alt: ... lower_step_init ]*
*/
<0x0d>
OP_INVOKE // *[ signature_slot, upper_sig_part, I_and_q, i_lower | alt: ... lower_sig_part ]*
<0> // [[ continue_loop ]] We'll feed this to OP_UNTIL, need it here for the 2ROT
OP_FROMALTSTACK // [[ lower_sig_part ]]
OP_2ROT
OP_ROT // *[ I_and_q, i_lower, continue_loop, signature_slot, upper_sig_part, lower_sig_part ]*
OP_CAT // [[ both_sig_parts ]]
OP_CAT // [[ signature_so_far ]]
OP_2SWAP // *[ continue_loop, signature_so_far, I_and_q, i_lower ]*
OP_1ADD // [[ next_i_upper ]]
<3> OP_ROLL // *[ signature_so_far, I_and_q, i_lower, continue_loop ]*
OP_ELSE
OP_FROMALTSTACK // By design: don't pollute alt (even if it works for most use cases and saves a byte)
// Remaining_message_hash and lower_j_end are now verified 0, but remaining_x could be longer due to malleation:
// Top: *[ signature_slot, upper_sig_part, I_and_q, i_lower, lower_j_end, remaining_x, remaining_message_hash ]*
OP_CAT
OP_CAT // If remaining_x was 0, this [[ should_be_zero ]]
OP_NOT // Assert that should_be_zero is 0 (consumed by OP_UNTIL)
OP_ENDIF
OP_UNTIL // *[ signature_slot, upper_sig_part, I_and_q, i_lower ]*
OP_2DROP
OP_CAT // [[ quantum_signature ]]
Open in Bitauth IDE: Quantumroot Schnorr+LM-OTS Vault →
Thanks for reading. Have a question? Feedback? Please let me know in the comments and/or send a pull request to the Quantumroot GitHub repo.