The extension has to build a signing payload, assemble an extrinsic and decode
a call well enough to show a user what they are approving. The obvious route was
@polkadot/api's codec. That is closed, and quantus/api#1 carries the tested
evidence:
- @polkadot/types caps fixed arrays at 2048 bytes, and ML-DSA signatures are
[u8;5261] and [u8;7219], so every Quantus extrinsic trips it
- api.rpc.chain.getBlock throws on every block of this chain, at the timestamp
inherent, because it reads the extrinsic preamble byte as a version when the
top two bits are a type tag
- it *guesses* that signed extensions it does not recognise contribute nothing
to the signed payload
The third is why this is a package rather than a patch. The guess is right
today — the registry says ReversibleTransactionExtension and
WormholeProofRecorderExtension are empty on both halves — and it is right only
by luck. This chain's encoding has changed between runtimes, transactionVersion
has gone 2 -> 3 -> 6 across four upgrades, and when the guess stops holding the
wallet keeps signing: valid signatures over a payload missing bytes the runtime
put there, reported by the chain as BadProof, which is also what it reports for
a wrong key.
So nothing here names a pallet, a call, an extension or a signature scheme.
Every type id is read from metadata the node produced by running
Metadata_metadata against the runtime WASM in a given block's state, the same
oracle blackbeard.observer has been decoding against across four upgrade
boundaries. encode_extensions walks the declared extensions in order and refuses
to build a payload when one that encodes to something has no value supplied —
a wallet that cannot sign is a bug report, one that signs the wrong bytes is a
support case nobody diagnoses.
Proven end to end on Heisenberg at spec 148: a balances.transfer_keep_alive
built entirely here, signed by @quantus/crypto under QUANTUS_EXTRINSIC, included
at block 1050475 and read back from that block — inherent at index 0 included,
which is the block @polkadot/api cannot decode at all.
Two notes carried over from @quantus/crypto, both load-bearing: decode_checked
walks with scale_decode's IgnoreVisitor before scale_value touches the bytes,
because scale_value sizes a Vec from the length prefix before decoding an item
and an aborted allocation leaves no Err to catch; and the build needs binaryen
123, since 105 silently corrupts the output.
Closes #3
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
64 lines
2.9 KiB
Bash
Executable File
64 lines
2.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Copyright 2026 @quantus/crypto authors & contributors
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
# Builds packages/quantus-crypto. Deliberately NOT part of build-wasm.sh: that
|
|
# script drives the nightly-2022-06-24 + xargo build that `wasm-crypto` needs,
|
|
# and this crate needs a modern compiler (inline `const {}` blocks, Rust >= 1.79).
|
|
# Keeping the two builds separate is what lets wasm-crypto stay byte-identical to
|
|
# upstream. See quantus/wasm#1.
|
|
#
|
|
# There is no asm.js step here. wasm2js over ML-DSA would be enormous and slow,
|
|
# and every context we ship into sets 'wasm-unsafe-eval', so wasm is always
|
|
# available where this runs.
|
|
|
|
set -e
|
|
|
|
# Which package to build. Two now — quantus-crypto and quantus-codec — built
|
|
# the same way from the same toolchain, so the script takes the name rather than
|
|
# being copied. `quantus_codec` is the crate name for `quantus-codec`: cargo
|
|
# wants underscores, npm wants hyphens.
|
|
PKG=${1:-quantus-crypto}
|
|
CRATE=$(echo "$PKG" | tr '-' '_')
|
|
BINDGEN_VER=0.2.128
|
|
|
|
WASM=packages/$PKG/build-wasm/${CRATE}_bg.wasm
|
|
OPT=packages/$PKG/build-wasm/${CRATE}_opt.wasm
|
|
|
|
echo "*** Building Rust sources"
|
|
# The toolchain comes from packages/quantus-crypto/rust-toolchain.toml, which
|
|
# pins the same channel the chain builds its runtime with.
|
|
(cd packages/$PKG && cargo build --target wasm32-unknown-unknown --release --locked)
|
|
|
|
echo "*** Converting to WASM"
|
|
./bindgen-quantus/wasm-bindgen \
|
|
packages/$PKG/target/wasm32-unknown-unknown/release/$CRATE.wasm \
|
|
--out-dir packages/$PKG/build-wasm \
|
|
--target web
|
|
|
|
# The glue is a build artifact but is checked in, so the package can be built
|
|
# without a Rust toolchain — the same reasoning as upstream checking in an empty
|
|
# bytes.js. Copy it back so the two never drift.
|
|
# Must run before the glue is copied into src/generated — see the script's own
|
|
# comment for why the fetch path cannot ship.
|
|
node ./scripts/strip-wasm-fetch-init.mjs packages/$PKG/build-wasm/$CRATE.js
|
|
|
|
echo "*** Updating checked-in bindings"
|
|
cp packages/$PKG/build-wasm/$CRATE.js packages/$PKG/src/generated/$CRATE.js
|
|
cp packages/$PKG/build-wasm/$CRATE.d.ts packages/$PKG/src/generated/$CRATE.d.ts
|
|
|
|
# binaryen-quantus, not binaryen. Upstream pins version_105 (2021), which predates
|
|
# the externref tables wasm-bindgen 0.2.128 emits: it "optimises" the table into
|
|
# something that fails at instantiation with
|
|
# `WebAssembly.Table.grow(): failed to grow table by 4`. The wasm is valid before
|
|
# wasm-opt and broken after, and nothing in the build says so — it only surfaces
|
|
# when a consumer tries to init. Same shape of problem as the two bindgens.
|
|
echo "*** Optimising WASM output"
|
|
./binaryen-quantus/bin/wasm-opt $WASM -Oz -o $OPT
|
|
|
|
# Must come before packing: tsc clears build/, which is where bytes.js lands.
|
|
./scripts/build-quantus-js.sh "$PKG"
|
|
|
|
echo "*** Packing WASM into baseX"
|
|
PKG_NAME=$PKG CRATE_NAME=$CRATE node ./scripts/pack-quantus-base.mjs
|