paralock-core
The protocol core of Paralock: trustless swaps and payments between ZEC and stablecoins on
EVM chains. A lock on Zcash (a P2SH HTLC, or a 2-of-2 multisig in private mode) and a lock on the EVM chain
(ParalockHTLC / ParalockAdaptorEscrow) share one secret, so either both sides settle or both refund.
This crate is the single implementation behind every Paralock SDK (Rust, Node via napi, browsers via wasm, wallets
via uniffi). It is pure and synchronous: no I/O, no clock, no network, no async. Randomness comes from a caller
supplied CryptoRngCore, the consensus branch id and the current time are always inputs, and every parser of
untrusted data returns a Result and never panics.
It is a byte-for-byte port of the TypeScript reference packages (@paralock/zcash, @paralock/adaptor,
@paralock/evm, @paralock/sdk): scripts, addresses, transactions, signature hashes, txids, escrow ids, claim
digests, adaptor pre-signatures and JSON messages are identical, which the test suite checks against the shared
cross-language vectors in spec/vectors.
Status: experimental. It has been through one security review, whose findings are fixed in 0.1.0 (see
CHANGELOG.md), but it has not had a formal audit. In htlc mode the Zcash leg is a transparent P2SH output, visible on chain. Don't put more money through it than you can afford to lose.
What's inside
| Module | What |
|---|---|
zcash |
Networks and Base58Check transparent addresses; script building and decompiling; the Paralock HTLC and the sorted 2-of-2 multisig templates with strict, byte-for-byte parsers; v5 transaction serialize/parse (shielded bundles kept opaque); ZIP 244 txids (Sapling and Orchard digests included) and transparent signature hashes for all six hash types; ZIP 317 fees; builders for P2PKH spends, HTLC funding/claim/refund and 2-of-2 spends |
adaptor |
ECDSA adaptor signatures on secp256k1 with Chaum–Pedersen DLEQ proofs: pre_sign (hedged nonces), pre_verify (returns a VerifiedPreSignature), adapt (only verified pre-signatures, low-S), extract, verify, DER/compact encodings, recovery ids, the 162-byte wire format |
evm |
ParalockHTLC commitments and ParalockAdaptorEscrow swap ids (keccak256(abi.encode(swap))), claim digests, hashlocks, canonical (v, r, s) claim signatures, ecrecover, EVM addresses, uint256 |
protocol |
Serde types for every wire message (Quote, AcceptRequest, AdaptorProposal, AdaptorProposalAccepted, AdaptorPreSignatureRequest, Swap, error bodies) with JSON identical to the TypeScript SDK, decoded through strict validating parsers; the composite safety checks of both parties (validate_quote, verify_accept_request, verify_adaptor_proposal, verify_proposal_accepted, verify_taker_pre_signature, check_funding_output, verify_htlc_lock, verify_adaptor_lock, complete_escrow_claim, complete_maker_claim); a validated TimeoutPolicy (MAINNET_POLICY / DEVNET_POLICY), the window rule and the network-upgrade rule; exact-in / exact-out pricing with the reference's rounding; privacy helpers |
keys |
secp256k1 private keys (zeroized), public keys, strict-DER/compact ECDSA signatures (RFC 6979, low-S), 32-byte secrets |
Installation
[]
= "0.1"
Features:
std(default): the OS random number generator (paralock_core::OsRng) and k256's precomputed tables.getrandom(part ofstd): justOsRng.
Without default features the crate builds for wasm32-unknown-unknown; pass any CryptoRngCore you have (for
example one backed by crypto.getRandomValues in your wasm bindings).
Examples
An HTLC: fund, claim, and learn the secret from the claim
use ;
use ;
#
Adaptor signatures (private mode)
use adaptor;
use KeyPair;
#
The EVM escrow: id, claim digest and the signature the contract accepts
use ;
use KeyPair;
#
Validating untrusted messages
use ;
use json;
let body = json!;
let err = parse_accept_request.unwrap_err;
assert_eq!;
assert_eq!;
assert_eq!;
// The window rule: the Zcash refund must outlast the EVM lock by the safety factor. It takes the refund height and
// the tip (a refund height at or below the tip is unsafe, never a wrapped-around window).
let tip = 3_000_000;
assert!;
assert!;
Checking a quote against what you asked for
use BTreeMap;
use ;
let quote = parse_quote.unwrap;
let ctx = QuoteContext ;
// The quote must answer exactly what you asked for: amount, chain and mode.
let asked = QuoteRequest ;
assert!;
let other = QuoteRequest ;
assert_eq!;
Compatibility and tests
tests/vectors.rsrecomputes every vector inspec/vectors/*.json(generated from the TypeScript packages) and the official ZIP 244 vectors; a missing vector file fails the test naming it, and a list in a vector file that no check reads failsevery_vector_list_is_consumed.tests/audit.rshas the security review's adversarial scenarios as regression tests;tests/audit_fuzz.rsmutates the script templates and DER signatures (PARALOCK_FUZZ_ITERATIONSfor a longer run).tests/interop.rs(ignored by default; needs Node) has the TypeScript packages pre-verify Rust adaptor pre-signatures and rebuild Rust transactions byte for byte:cargo test -p paralock-core --test interop -- --ignored.tests/regtest.rs(ignored by default) broadcasts Rust-built HTLC and 2-of-2 transactions to a local Zebra regtest:cargo test -p paralock-core --test regtest -- --ignored --test-threads=1.- Property tests cover adaptor round trips, transaction round trips, template parsers (no lookalike script is ever
accepted) and parser robustness on random input.
PROPTEST_CASESoverrides the case counts.
Security notes
unsafeis forbidden. Private keys, HTLC preimages and adaptor secrets are zeroized on drop, never printed byDebug, and compared in constant time.- Every signature this crate produces is low-S, and every verifier rejects high-S. Zcash signatures are RFC 6979
deterministic (identical to the TypeScript reference); adaptor pre-signatures use a fresh hedged nonce
(
sha512(tag ‖ key ‖ digest ‖ Y ‖ 32 random bytes)), so even a repeating RNG cannot reuse a nonce across messages. - The HTLC and 2-of-2 parsers accept only the exact templates with valid curve points; only the six ZIP 244 sighash
types exist; CompactSizes must be canonical; amounts are checked against
MAX_MONEYwith checked arithmetic. - Spends of a lock (claims, refunds, 2-of-2 spends) never expire (expiry height 0), because they are signed long before they are broadcast; fundings require an expiry height, and outputs are paid to addresses checked against the network, never to raw scripts.
- Values the builders and checks rely on can only be built validated:
KeyPair(its public key is derived),TimeoutPolicy,Htlc,Multisig2of2,MultisigSpend,VerifiedPreSignature,ClaimLimits. Wire messages decoded with serde go through the same validating parsers asparse_*. - Use the composite checks in
protocol: they check everything a step requires against the stored messages (the taker'sverify_adaptor_lockcomputes the claim digest itself, for its own chain and escrow, from the lock read on-chain). A claim that reverts still reveals the secret (the preimage, or the completed signature from which the maker extractsy): re-check the lock right before claiming and simulate the claim (eth_call) first. - Mixed-case EVM addresses parsed with
EvmAddress::parse/str::parsemust carry a valid EIP-55 checksum. - RNG failures in pre-signing are errors (
NonceGenerationFailed), never panics;try_randomconstructors exist for keys and secrets.
License
MIT