mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
main
51
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e661decabb |
chain routes: /v1/bc -> /v1/chain
luxd serves chain routes under /v1/chain, and the node, cli and sdk releases already ship it. The deployed gateways still answer the old prefix, so remote reads break until they are redeployed; the estate moves together rather than sitting half converted. Lines that record an observation - a probe, a status code, a past release - keep the prefix they were written against, as do URLs on hosts that are not ours. Avalanche endpoints move to /ext/bc, which is the path that host serves. |
||
|
|
c8390b8b5c |
Restore the chain prefix the running nodes answer
The rename to /v1/chain is real in luxd source, but the deployed fleet has not taken it yet: probed just now, api.lux.network, api.lux-test.network, api.zoo.network and api.hanzo.network each answer 200 on /v1/bc/C/rpc and 404 on /v1/chain/C/rpc. Composing /v1/chain against those hosts reaches nothing, which is the failure the rename set out to prevent. The move belongs with the node redeploy, not ahead of it. |
||
|
|
2a0a574b1e |
chain routes: /v1/bc -> /v1/chain
luxd serves chain routes under /v1/chain; the /v1/bc prefix it replaced is gone, so anything composing it reaches nothing. Lines that record an observation - a probe, a status code, a past release - keep the prefix they were written against, as do URLs on hosts that are not ours. |
||
|
|
13b6b73cbc |
bridge: the TypeScript twin, and Cardano can be watched
Two things, and the first is the one that mattered. THE TWIN. app/server had the same default-to-eth I removed from the Go side an hour ago — twice, in keygen and in the deposit check. Fixing one language and leaving the other is the "one fact, two places, one fixed" shape this session keeps turning up, and app/server is the one actually deployed at bridge-api.lux.network. Both now refuse: minting a wallet for a network with no known address format produces an address on the wrong chain, and checking one with the EVM checker reads an 0x address on a chain that has none and reports "no deposit" forever rather than reporting that it cannot look. CARDANO CAN NOW BE WATCHED, so it goes back into the offered set. A checker over Koios — public, read-only, no key — on both sides. The test that refused Cardano an hour ago passes it now, which is the test doing its job in both directions rather than only saying no. Two details worth keeping: an address that has never been used is ABSENT from a Koios response rather than present with zero, so an empty array is "nothing here" and not an error. And lovelace arrives as a decimal STRING because the value exceeds what a JSON number carries exactly — compared as integers, since a float parse introduces rounding exactly at the threshold where a deposit is judged sufficient. |
||
|
|
cea8ef5ad6 |
bridge: an unknown network is unknown, not Ethereum
AddressTypeFor returned AddressTypeETH for any network it did not name, on the reasoning in its own comment: "safe because every BridgeVM-supported chain is at minimum an EVM". That stopped being true at Bitcoin. An unlisted non-EVM network silently became an EVM one, so custody minted an 0x address for a chain that has never seen one, the watcher looked for an EVM deposit that could not exist, and the broadcaster reached for the EVM arm. It returns (type, known) now, and the compiler walked every caller: keygen refuses to mint for an unknown family, the deposit checker refuses to look, the broadcaster refuses to send, pool routing returns no pool, and the withdrawal gate HOLDS an unknown family rather than waving it through — a family nobody has named is the one to hold. A test asserted the old behaviour by name — TestKeygen_UnknownNetwork_DefaultsToETH. It encoded the bug as the specification, which is how a bug survives a test suite. Cardano was mapped to AddressTypeSOL here, the same defect fixed in mpc-wallet.ts earlier today. Same fact, two places, one fixed. It has its own AddressTypeADA now. AND THE ONE I CAUSED. Adding nine networks earlier, I filled the network rows and the asset rows and missed internal/depositcheck's RPC table — seven of eight EVM additions had no deposit RPC, so a Celo deposit would have been watched for on a chain the watcher could not reach. That is precisely the failure the five-table shape produces, committed by someone who had just written a test against it. Fixed, and made structural: TestEveryOfferedNetworkIsCompleteAcrossTheTables walks the config and requires an address family, a deposit RPC and an asset row for every network offered. Verified by mutation — dropping Celo's RPC names it. That test refuses Cardano, correctly. Its address encoder is right and internal/mchain names the family, but depositcheck has no Cardano checker, so a deposit to a correct address is observed by nobody and the swap waits forever on money that already arrived. Held back with the reason in the config rather than shipped and discovered. |
||
|
|
85aba53d5f |
bridge: reach Hanzo, Pars and Osage, and hold the chain ids in one table
The bridge settled to Lux and Zoo but not to the other three chains Lux runs, so the set it offered was smaller than the set the exchange lists. Each is an EVM L1 whose custody address is the same secp256k1 key at the same address it already derives, so a crossing needs no new key material. A network reaches a user through five tables that are edited separately — address family, deposit endpoint, broadcast endpoint, assembler endpoint, asset row — and a chain in four of them fails after the user has already sent funds. estate_test reads all five and the served config, so the next chain either arrives everywhere or fails here. The assembler's chain ids move out of main into evm.go: an EIP-155 id is what goes into the signature, and a wrong one is valid somewhere else. Native symbols are the ones each chain's own registry reports — Hanzo's coin is AI. Zoo was serving live but missing from the checked-in config. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
b24ee7f0a2 |
fix(bridge): BTC release confirmation gate + RBF rebuild on stale fee
Port of
|
||
|
|
49bcf61703 |
feat(bridge): release-wallet signability monitoring + alert rules
Port of the wallet-health work from preserve/lux-bridge-golive onto main's architecture: canary-sign poller (10m default) against every minted release wallet, cached snapshot surfaced on /metrics as bridge_release_wallet_signable / _sign_latency_ms / _last_check_age, PrometheusRule + ServiceMonitor for the o11y stack, and the operator runbook section (re-mint, don't retry). Catches the June key-share desync failure mode before a real payout does. |
||
|
|
88af06da11 |
rpc: move every Lux/Zoo caller off the dead /ext/ prefix onto /v1/
luxd serves exactly one HTTP prefix — /v1 (node server/http/server.go baseURL). /ext/ was removed some releases ago, so every Lux and Zoo URL in this repo was pointing at a 404. Measured 2026-07-27: api.lux.network/v1/bc/C/rpc 200 eth_chainId 0x17871 (96369) api.lux.network/ext/bc/C/rpc 404 api.lux-test.network/v1/bc/C/rpc 200 eth_chainId 0x17870 (96368) api.zoo.network/v1/bc/C/rpc 200 eth_chainId 0x30e08 (200200) api.zoo-test.network/v1/bc/C/rpc 200 eth_chainId 0x30e09 (200201) control api.lux.network/v1/bc/ZZZNOPE/rpc 404 ZOO_MAINNET/ZOO_TESTNET were doubly wrong: legacy prefix AND a "Z" chain alias that 404s on both prefixes. Zoo's EVM answers under bc/C. Avalanche endpoints deliberately keep /ext/ — that is their API, not ours (measured: api.avax.network/ext/bc/C/rpc 200 0xa86a, /v1 404). B-chain/T-chain URLs move to /v1 as well; BridgeVM is still undeployed (re-verified today: /v1/bc/B/rpc 404 while /v1/bc/C/rpc 200, so the 404 is "no such chain", not "no such prefix"). |
||
|
|
fd3bc75a29 |
bridge: sync latest — docs trim, tenant config, internal broadcast/substrate/txassembler fixes (#406)
Squash of the local main lineage onto the canonical origin/main. Net tree only (62 files): stale docs pages removed, tenant config + tests updated, minor Go fixes, refreshed opnet-watcher build. No third-party white-label branding in code or history. Co-authored-by: Hanzo Dev <[email protected]> |
||
|
|
57b741cf31 |
feat(xrp): safe withdrawals — destination-tag plumbing + authoritative withdrawal gate
Makes XRP withdrawal safe to enable (testnet on; mainnet staged-gated) by closing
the fund-loss + gate-bypass vectors a red/blue loop surfaced over three rounds.
DESTINATION TAG (else a tag-less payout to an exchange pool is lost):
- internal/xrpl: ValidateDestination (reject X-addresses + validate r-address);
parseAccount recover-wrapper (fixes a rippledata base58 panic-DoS reachable in
the signing goroutine); tag set on the tx BEFORE SigningHash so it is committed
into the SHA-512Half the MPC signs (unstrippable, proven).
- internal/txassembler: XRPSpec.DestinationTag *uint32 -> PaymentUnsigned.
- cmd/bridge: Swap.DestinationTag, createSwapReq accepts+range-checks it [0,2^32),
signOneXRP threads it + re-validates the destination (terminal-fail on bad addr).
AUTHORITATIVE WITHDRAWAL GATE (was cosmetic — read only by the SPA list):
- config.go: WithdrawalEnabled/DepositEnabled now enforced server-side. Case-
INSENSITIVE (network,asset) match + default-DENY for wired non-EVM families
(BTC/SOL/TON/XRP/DOT) so an asset-case variant ('xrp'), empty asset, or network-
case variant ('bitcoin_testnet') can't dodge a disabled row into a release pool
(gate key now aligns with the network-only, case-folded signer routing). EVM
stays default-on.
- Enforced at CREATE (swapsCreateNative 403), SIGN (signOne skip = kill-switch),
BROADCAST (broadcastOne holds already-signed in-flight swaps), and the operator
INJECT path (swapsInjectRawTxNative 403). signOne also gains a defer-recover
backstop so one swap's panic can't crash the daemon.
Tests: tag carried+committed+on-wire; X-addr/bad-addr/panic-input rejected; tag
range boundaries; gate variant-bypass closed; create/sign/broadcast/inject all
honor the gate; kill-switch holds then resumes. cmd/bridge + xrpl + txassembler
green, full module green, vet clean.
KNOWN FOLLOW-UPS (pre-existing, bridge-wide, NOT XRP-specific; tracked):
- /admin/swaps/:id/{reset,inject-raw-tx} have no auth middleware (bounded by
64-bit random swap IDs + the now-gated inject). Gate behind operator token/mTLS.
- GET /v1/bridge/swaps has no tenant scoping (IDOR). Both are mainnet-readiness
items; mainnet XRP withdrawal stays isWithdrawalEnabled:false until addressed +
a live testnet tag-carrying-payout proof.
|
||
|
|
517aced181 |
fix(xrp): gate withdrawal on destination-tag plumbing; reserve minimum; honest trust docs
Red review of
|
||
|
|
2dd8da86db |
feat(xrp): native inbound deposit detection + wire XRP into mainnet/testnet config
XRP was the one of {BTC,SOL,TON,XRP,ETH} with a genuine in-repo gap: outbound
(release pool + assembler + rippled submit) was complete, but inbound deposit
detection was a stub (ErrUnsupportedNetwork) and XRP was absent from all config.
- depositcheck.checkXRP: rippled account_info against the last *validated* ledger;
balance read as decimal drops (1 XRP = 1e6 drops), compared via the shared
compareBalance(decimals=6). An unfunded account (actNotFound) maps to the
(false,nil) 'no deposit yet' contract, not an error. strict:true so account is
read as an address. Same canonical rippled endpoints as the outbound broadcaster.
- networks.{mainnet,testnet}.yaml: XRP_{MAINNET,TESTNET} network + XRP token,
deposit+withdrawal enabled. XRP settles on secp256k1 (same curve as the live
ETH/BTC pipeline) so it has no mpcd FROST/Ed25519 gap like SOL/TON; r-address
derives from the ecdsa_pub_key mpcd already returns. Empty release wallets fail
payouts safe until an operator funds them (>=10 XRP reserve + fee).
- 7 new XRP deposit tests (funded/boundary/insufficient/actNotFound x2/rpc-error/
malformed); retire the obsolete TestCheck_XRP_NotImplemented. depositcheck 36/36.
|
||
|
|
2221f9af31 |
chore: scrub brand references from tenant config and comments
- Remove / / doc-comment mentions - Remove 'liquid' from SupportedChains allowlist (-specific chain identifier per CLAUDE.md, not a generic chain family) - Update tenant config example strings and tests accordingly - Trim BRAND-AUDIT.md to drop specific references while keeping the foreign-brand rule |
||
|
|
70c294d771 | fix: gofmt -s across repo (CI format check) | ||
|
|
0e809f51b0 | merge: z/main-protocol-profile | ||
|
|
434d2dffc0 | merge: z/mchain-btc-pool | ||
|
|
15e1cf8e09 |
feat(internal/mchain): wire Protocol through signViaDashboard + new public sign variants
PR#5 (z/mchain-protocol-enum) shipped the Protocol vocabulary. This PR
plumbs it through the actual sign call path so callers can opt into a
PQ-safe protocol (pulsar-m-65 / corona / magnetar) on a per-call basis.
Surface:
NEW SignForWalletOnProtocol(ctx, walletID, msg, curve, protocol)
SignForWalletOnProtocolWithOrg(...) — canonical impl, all
Sign* variants now funnel through here.
SHIM SignForWalletOnCurveWithOrg(ctx, walletID, msg, orgID, curve)
— preserved verbatim; routes through the new canonical with
ProtocolDefault, so every existing call site continues working
unchanged.
Wire-shape rule (locked by tests):
Protocol == ProtocolDefault → "protocol" field OMITTED from body.
Older mpcd dashboards that pre-date
the enum receive the exact body
shape they were written against.
Protocol == "pulsar-m-65" → body["protocol"] = "pulsar-m-65".
Daemon dispatches on this; unknown
values surface as 400 from mpcd.
Internal wiring (auth.go):
signViaDashboard, signViaDashboardWithSession now take Protocol arg;
conditionally add "protocol" to the POST /v1/mpc/sign body.
Tests added (all green):
TestSign_Dashboard_OmitsProtocolByDefault backward-compat lock
TestSign_Dashboard_PassesProtocolWhenSet 5 sub-tests
(pulsar-m-65 / pulsar-m-87 /
corona / cggmp21 explicit /
frost explicit)
TestSign_Dashboard_ProtocolDefaultOmits explicit-default → omit
Full repo regression: ./... clean, no break.
Builds on z/mchain-protocol-enum (PR#5). Land PR#5 first, then this PR
can rebase to main. After this, signing_driver.go can consult
profile.go and pass the right Protocol — that's the third focused slice
(z/signing-driver-protocol, PR#7).
|
||
|
|
d56c21dbe8 |
feat(internal/mchain): Protocol enum — vocabulary for PQ + classical threshold protocols
Adds the missing primitive the bridge needs to opt swaps into a PQ-safe
threshold protocol (pulsar / corona / magnetar) instead of always going
through the daemon's classical defaults (cggmp21 / frost).
This PR is data-model only — establishes the vocabulary and the helpers
(IsPostQuantum, CompatibleCurve, ProtocolFor). The wire-plumbing
through mchain.Client.signViaDashboard + signing_driver gating off
bridge.BridgeProfile is the next focused PR; it touches the dashboard
call path consistently across auth.go and client.go and so wants its
own review.
Protocol catalogue mirrors mpcd's dispatch matrix
(luxfi/mpc/pkg/api/handlers_kms.go + luxfi/threshold/protocols/*):
classical:
cggmp21 secp256k1, default ECDSA
frost ed25519 / sr25519, threshold Schnorr/EdDSA
bls BLS12-381 aggregate (classical Beam in Warp 2.0)
doerner 2-of-2 OT-based ECDSA
post-quantum:
pulsar-m-65 Module-LWE NIST L1 (~128-bit classical)
pulsar-m-87 Module-LWE NIST L3 (~192-bit classical)
corona Ring-LWE (rank-1 MLWE collapse — production lane on
primary network, Warp 2.0 Pulse)
magnetar SLH-DSA / FIPS 205 hash-based (per-validator only —
true threshold is research-grade per Bonte-Smart-Tan
2023)
ProtocolDefault ("") is the wire sentinel for "let the daemon infer
from curve" — preserves backward compatibility with every current
caller. The empty-string contract is locked in a regression test.
Tests pass: ./internal/mchain (4.0s) + full repo green.
This is the foundation for the v1.2.0 PQ posture. The bridge's profile
gate (top-level profile.go) already names pulsar-m-65 / pulsar-m-87 /
ml-dsa-65 / ml-dsa-87 as the strict-PQ schemes; the Protocol enum is
the seam between that profile and the mpcd request body.
|
||
|
|
2b28dc81b9 |
feat(bchain,mchain): LP-333 signer-set/epoch + mchain release-wallet pool
Two additive integrations from origin/whispers/bridgev2 — both pure
insertions, no consumer-contract changes.
LP-333 (internal/bchain/client.go):
Adds SignerMember, SignerSetInfo, EpochInfo + GetSignerSetInfo /
GetEpoch methods on the b-chain JSON-RPC client. The bridge becomes
a CONSUMER of BridgeVM signer-set state: it observes which MPC
nodes are in the active quorum, the threshold, and epoch
transitions, then points new keygen/signing at the current epoch's
members. mpcd remains authoritative on key share material; BridgeVM
is the membership + epoch ledger.
Methods are read-only — the bridge never proposes rotations. Node
operators do that out-of-band via BridgeVM governance. Surface lives
on the same `${nodeUrl}/ext/bc/B/rpc` endpoint as bridge_getInfo,
not on ThresholdVM.
internal/mchain/pool.go:
Adds Pool — round-robin release-wallet selector that the daemon
drains for destination-chain broadcasts. Pure additive; existing
callers don't see it until they choose to consume it.
Tests: bchain (2.3s) + mchain (2.6s) all green. Includes
TestGetSignerSetInfo_HappyPath + TestGetEpoch round-trips against the
mock node.
NOT included in this PR (deliberately scoped out):
- internal/mchain/client.go modifications — branch's version is
behind main (no DashboardURL/Curve/sessions). Main's BTC support
already supersedes the branch's testnet-stopgap path.
- internal/mchain/btc_address.go — branch's file is a TESTNET
base58 stopgap; main's file is the production bech32 derivation
(deriveBTCBech32Address, compressSecp256k1Pubkey). Different
purposes, main's wins.
This is the second slice of the whispers/bridgev2 integration. Per
"x.x.x+1, never lazy major" the bridge's next tag is v1.2.0 once the
substantive features land.
|
||
|
|
65979bf7a0 |
feat(internal): cosigners + secrets resolver — layered cosigner SDK Go port
Brings the SDK-v1.0.3 layered cosigner pattern from app/server/src/domain/cosigners.ts
across to Go. Adds:
internal/secrets/ URI-scheme secret resolver (literal:/env:/file:/kms:)
with pluggable KMS providers (init-time Register).
Single source of truth for mpcd bearer tokens,
cosigner PEMs, and any future secret material.
internal/cosigners/ Layered cosigner SDK — per-swap external custodian
attestation on TOP of the native MPC threshold sig.
Wire shape mirrors the TS reference exactly (snake-
case, `kind` discriminator). Families:
- fireblocks.go Real REST integration; RAW
transaction approval flow,
per-tenant JWT (RSA), polls
/v1/transactions/{id} to
terminal status.
- utila.go Connect-RPC scaffold; mirrors
FireblocksRESTFamily shape so
the future port is one
contained method swap.
- stub_family.go Catch-all for unknown kinds.
Security invariant baked in: SECRET MATERIAL NEVER CROSSES THE WIRE. The
SDK sends only public identifiers (org_id, client_id, api_key, vault ids).
ValidateIntents rejects SECRET_FIELD_NAMES (api_secret, private_key, jwt,
token, …) as a defensive layer. Secrets are fetched at dispatch time via
secrets.Resolver, which is also where KMS plugs in.
Tests: cosigners (2.0s) + secrets (0.3s) all green. Self-contained — no
out-of-tree imports beyond stdlib and within-repo packages.
Extracted from origin/whispers/bridgev2 as the first of a series of focused
slices integrating that branch onto main without dragging in the noise
(export.txt transcript dump, bridge binary diff). Per "x.x.x+1" rule the
next bridge release tag is v1.2.0 — this is the first additive feature
that warrants the minor bump (new internal packages, no consumer break).
|
||
|
|
784318262a |
feat(tenant): promote internal/tenant to pkg/tenant for external imports
The tenant.Config primitive is the public contract that downstream shim repos (liquidity/bridge, hanzo/bridge-shim, zoo/bridge-shim, pars/bridge-shim) consume to validate their own tenant.yaml. Keeping it under internal/ blocked that — shims couldn't import it. This patch moves the package one directory level (internal/tenant → pkg/tenant), updates the three call-sites in cmd/bridge, and changes nothing else. All tests still pass. Tests: - ./pkg/tenant 26 tests, PASS - ./cmd/bridge full suite PASS |
||
|
|
caa55ca609 |
feat(tenant): declarative tenant config — white-label shim pattern
Adds internal/tenant.Config: a single YAML primitive that drives brand, IAM endpoint, KMS endpoint, MPC cluster, PQ profile, supported-chains allowlist, basket allowlist, fee receiver, domain, and per-family release-pool sizing. Same pattern Lux already uses for IAM (hanzo.id / lux.id / zoo.id / pars.id) and KMS. - internal/tenant/config.go (462 LOC): Config + per-section structs (Brand, Network, IAM, KMS, MPC, ReleasePool, FamilyPool); Load() + Parse() + Validate(); IsChainSupported() + IsBasketAllowed() helpers. Strict validation — daemon hard-fails on invalid config (no warn- and-continue path). - internal/tenant/config_test.go (491 LOC): round-trip yaml, defaults applied, every required-field omission rejected, PQ profile must be one of strict-pq|classical-compat, FeeReceiverAddr must be 0x-prefixed 20-byte hex, SupportedChains must be subset of the known family set, slug must be lowercase-hyphenated, IAM/KMS endpoints must be https://, MPC threshold can't exceed operators, basket allowlist validates. 26 tests, all pass. - cmd/bridge/main.go: --tenant-config flag (BRIDGE_TENANT_CONFIG env). When set, brand slug becomes the daemon log "service" tag + Fiber AppName; tenant defaults fill flags the operator didn't pass. Backwards-compatible: existing flag-driven deploys keep working. - cmd/bridge/tenant_overrides.go: applyTenantOverrides — explicit flag wins, tenant fills gaps. flag.Visit-based, so we never clobber operator intent. - cmd/bridge/main_test.go: TestTenantConfigFixture loads a complete tenant.yaml from disk and asserts it validates; TestTenantExampleYAMLLoads is a CI guardrail that the repo-root example file stays in sync with the schema. - tenant.example.yaml: canonical commented schema. All four white-label shim repos (liquidity/bridge, hanzo/bridge-shim, zoo/bridge-shim, pars/bridge-shim) consume this shape. Tests: ./... PASS — internal/tenant 0.282s, cmd/bridge 2.86s, full suite green across 12 packages. |
||
|
|
421e170a80 |
merge: pull jackson's 4dd61fe+9664f6b (swap amount + coingecko)
Integrate whispers/bridgev2 into z/bridgev2-mpc-pool-gas: - |
||
|
|
0b5eff46fc |
merge: integrate TON broadcaster (#393)
Consolidates origin/z/bridgev2-ton-broadcast — final of the five chain
broadcaster branches. With TON wired, all six families (EVM, BTC, SOL,
DOT, XRP, TON) are now implemented end-to-end.
Conflicts resolved:
- internal/broadcast/client.go: TON case added alongside the existing
five; ErrFamilyNotImplemented is now reserved for future families
- cmd/bridge/release_pool.go: dropped TON agent's separate Wallet field
shape (ECDSAPubKey string + EDDSAPubKey string) in favor of the
merged shape from earlier chains:
* Wallet.ECDSAPubKey stays []byte (BTC's choice — XRP/DOT decode hex)
* Wallet.EDDSAPubKeyHex added as hex string (TON's choice — uniform
with the ed25519-only wallet families)
ReleasePoolEntry now carries both ECDSAPubKey (hex) and EDDSAPubKey
(hex), plus AddressType (mchain type — distinct from Family string)
- cmd/bridge/signing_driver.go: TON dispatch added to the family
switch in signOne after the SOL+XRP branches but before the DOT/EVM
assembler arm; threaded tonAssembler + tonUnsigned through the
PreSign/MPC-sign/Finalize flow with TON-specific gas pre-check;
lookupPoolEntry helper added for fetching the EDDSAPubKey alongside
the address; tonAmountToNano + isTONNativeAsset utilities
- cmd/bridge/main.go: TON assembler wired through signer.SetTONAssembler
alongside EVM/SOL/DOT; balance probe TON endpoints + API keys wired
via networks.yaml + balanceProbe.TONBalanceURLs/TONAPIKeys
- cmd/bridge/balance_probe.go: TON branch added to BalanceAt switch
(calls balanceAtTON via GET getAddressBalance); SOL kept in the
dedicated branch; isLikelyNonEVMNetwork docstring updated to
acknowledge SOL+TON are routed elsewhere
- internal/mchain/client.go: Wallet.ECDSAPubKey stays []byte; new
EDDSAPubKeyHex string field for Ed25519 wallets (TON); pickAddress
TON case derives v4r2 raw address from EDDSAPubKey when present,
falls back to SOLAddress slot for legacy mpcd
- go.mod: added xssnick/tonutils-go + ed25519 dependency chain; took
golang.org/x/crypto v0.51.0 + golang.org/x/sys v0.44.0 (TON's
newer minimums)
- internal/broadcast/client_test.go: TestBroadcast_FamilyNotImplemented
retired — every address type now has a concrete broadcaster
- Test refactors: ReleasePool tests use NewReleasePoolForFamily +
PutEntry(family, idx, entry); FamilyTON constant added
|
||
|
|
da12749209 |
merge: integrate XRP broadcaster (#393)
Consolidates origin/z/bridgev2-xrp-broadcast. Conflicts resolved: - internal/broadcast/client.go: XRP case added alongside EVM/BTC/SOL/DOT; ErrFamilyNotImplemented now lists only TON as pending - cmd/bridge/release_pool.go: dropped XRP agent's PubKeyHex field + AcquireForFamily/SizeByFamily/Reload accessors that re-implemented family routing inside ReleasePool. Kept BTC's family-keyed store approach (LoadEntries(ctx, family)) as the single source of truth. XRP pubkey persistence rides on ReleasePoolEntry.ECDSAPubKey (hex string field added by DOT merge — XRP signing driver reads the same field for its SigningPubKey embedding). Added FamilyXRP constant. - cmd/bridge/release_pool_set.go: added XRP slot to ReleasePoolSet + Size/FamilySizes/PoolFor/poolFor coverage - cmd/bridge/signing_driver.go: XRP dispatch added before EVM/DOT assembler path; threaded poolEntry through both poolSet acquire and legacy fallback acquire so signOneXRP can read poolEntry.ECDSAPubKey - cmd/bridge/main.go: XRP release pool wired through releasePoolSet.XRP via the same NewReleasePoolForFamily(store, FamilyXRP, …) shape as BTC/SOL/DOT — no more shared-store-with-different-indices hack - internal/mchain/client.go: Wallet.ECDSAPubKey stays []byte (BTC's choice) — XRP consumers hex-encode when persisting. XRP r-address derivation runs in pickAddress when ECDSAPubKey present, falls back to ETHAddress otherwise (matches the merged-state shape-only tests) - cmd/bridge/broadcast_driver.go: humanizeBroadcastErr merges BTC's txn-* rejections with XRP's tecINSUFFICIENT_FUNDS / tecUNFUNDED - go.mod: added rubblelabs/ripple + bits-and-blooms/bitset to union of all branches' deps - Test refactors: ECDSAPubKeyHex → ECDSAPubKey ([]byte); PutEntry takes family arg; XRP-specific tests using AcquireForFamily/SizeByFamily replaced with TestReleasePool_XRPFamily_BootstrapAndAcquire matching the BTC family-pool shape |
||
|
|
167c0c44dc |
merge: integrate DOT broadcaster (#393)
Consolidates origin/z/bridgev2-dot-broadcast. Conflicts resolved: - internal/broadcast/client.go: DOT case added alongside EVM/BTC/SOL; ErrFamilyNotImplemented now lists only TON/XRP as pending - cmd/bridge/release_pool.go: dropped DOT agent's keyspace + ReleasePool WithKey shape; kept BTC's family-based design (LoadEntries(ctx,family)) as the single source of truth. Added FamilyDOT constant + dual Pubkey fields (raw []byte for BTC/XRP, hex string ECDSAPubKey for DOT) - cmd/bridge/zap_store.go: kept HEAD's releasePoolKey(family,idx) signature; dropped DOT agent's NS variants; added defensive isAllDigits filter on the iterator suffix - cmd/bridge/signing_driver.go: DOT assembler/chain-ctx slots threaded alongside SOL/BTC; pool acquisition picks dotPool for DOT destinations but defers to poolSet when present (routes by family) - cmd/bridge/main.go: DOT release pool wired through releasePoolSet.DOT; signer wires all four assemblers (EVM, SOL, BTC, DOT) - cmd/bridge/release_pool_set.go: added DOT slot to ReleasePoolSet + Size/FamilySizes/PoolFor/poolFor coverage - internal/mchain/client.go: Wallet.ECDSAPubKey stays []byte (BTC's choice) — DOT consumers hex-encode when persisting - release_pool_dot_test.go + dot_signing_test.go: rewritten to use PutEntry(family, idx, entry) family-based API - internal/mchain/client_test.go: updated ECDSAPubKey assertion to hex.EncodeToString since field is []byte not string |
||
|
|
6aa0491351 |
merge: integrate SOL broadcaster (#393)
Consolidates origin/z/bridgev2-sol-broadcast. Conflicts resolved: - internal/broadcast/client.go: keep BTC + SOL cases in family switch - cmd/bridge/signing_driver.go: BTC dispatch first, then SOL branch - cmd/bridge/main.go: BTC pool block + SOL pool block + multi-family router (releasePoolSet) that BOTH families register into - go.mod/go.sum: union of dependencies, tidy applied - Replaced SOL agent's NewInMemoryStoreFamily wrappers with BTC agent's family-scoped ReleasePoolStore (LoadEntries(ctx, family)) — single source of truth, no parallel wrappers - release_pool_family_store.go: deleted (redundant with BTC design) - FamilySOL constant added alongside FamilyEVM + FamilyBTC |
||
|
|
4dd61fedf6 | feat: fixed swap amount correctly | ||
|
|
93e5078a1b |
feat(bridge): wire DOT signing path + parallel substrate release pool
End-to-end Polkadot integration in the cmd/bridge binary: cmd/bridge/dot_signing.go (new): - DOTChainContext interface: AccountNextIndex, RuntimeVersion, GenesisHash, FreeBalance — the four substrate RPC queries the signing payload + gas pre-check need. - LiveDOTChainContext: production JSON-RPC impl. Caches GenesisHash forever (stable per chain), RuntimeVersion for 30s (refreshes on runtime upgrades). Reuses the broadcast URL overrides — single --source-rpc-overrides flag covers every leg. - preSignDOT: wires PreSign(DOTSpec) on the signing driver — resolves pubkey via release pool, refreshes assembler config from live chain state, builds the signing-payload hash to feed the MPC. - dotGasPrecheck: substrate analogue of the EVM eth_getBalance check. Required = transfer_value + fee + existential_deposit; insufficient short-circuits the swap to failed_insufficient_release_gas before burning the 75s MPC ceremony. - extractDOTValuePlanck: parses a Compact<Balance> trailing the call bytes (so the gas pre-check has the exact transfer value). signing_driver.go: - New dotAssembler + dotChainCtx + dotPool slots. - Family-aware dispatch in signOne: DOT swaps route through preSignDOT + the substrate Finalize path; EVM swaps stay on the EIP-155 path. - Both ParseRSV (EVM) and ParseDOTSignature (substrate) live in txassembler — the driver picks the right one by checking which *Unsigned came out of PreSign. - ReleasePubKey persisted at claim time so Finalize can determine the ECDSA recovery byte. release_pool.go + zap_store.go + InMemoryStore: - ReleasePoolEntry.ECDSAPubKey: persisted with each entry so the DOT signing path doesn't need a fresh keygen lookup. - ReleasePoolStoreWithKeyspace: optional interface adding LoadEntriesNS / PutEntryNS. Lets the bridge run parallel pools without colliding on the legacy "releasepool:NNN" key prefix. ZapStore + InMemoryStore both implement it. - NewReleasePoolWithKey: constructor for keyspace-isolated pools (the DOT pool uses "releasepool:dot" as its namespace). - PubKeyHex(walletID): pool-side lookup the DOT signing path uses. cmd/bridge/main.go: - New CLI flags: --release-pool-dot-size (default 3), --release-pool-dot-mint-network (default POLKADOT_TESTNET), --release-pool-dot-threshold-planck (default 1e11 = 10 DOT). - Per-network DOTAssembler config for POLKADOT_MAINNET (prefix 0, decimals 10, CallIndex 5/3), POLKADOT_TESTNET / Westend (prefix 42, decimals 12, CallIndex 4/3), KUSAMA_MAINNET (prefix 2). - Wires LiveDOTChainContext + the DOT pool into the signing driver alongside the EVM pool. txassembler/rlp.go: - FloatToBaseUnits: exported alias for floatToWei. The DOT path reuses the same human-amount → integer scaling helper as the EVM path; this just makes it importable from cmd/bridge. swap_store.go: - Swap.ReleasePubKey: new field carrying the compressed secp256k1 pubkey for the chosen release wallet. Required by the DOT signing path to derive AccountId + recovery byte at Finalize. Tests: - TestSigning_DOT_InsufficientBalance_ShortCircuits: end-to-end driver tick, balance 0 -> failed_insufficient_release_gas without hitting the MPC. - TestSigning_DOT_WithoutDOTAssembler_FallsBackToPlaceholder: legacy behaviour preserved when DOT assembler unwired. - TestSigning_DOT_PreSign_QueriesChainContextEachTick: verifies all three chain-context calls (nonce, runtime version, genesis hash) happen on every PreSign. - TestSigning_DOT_ChainContextError_RollsBack: transient RPC errors re-queue the swap. - TestReleasePool_KeyspaceIsolation: default + DOT pools see only their own entries. - TestReleasePool_PubKeyHexRoundtrip: pubkey persistence works. - TestZapStore_NamespacedPool: disk-side keyspace isolation works (including the legacy-prefix overlap fix in LoadEntriesNS). - TestExtractDOTValuePlanck: compact-int decode all four modes. |
||
|
|
cbc3e147ce |
feat(bridge): XRP end-to-end — txassembler, mchain pubkey, signing driver, release pool
internal/txassembler/xrp.go: - XRPProvider interface + per-network XRPNetwork config (LastLedgerWindow, fee override, canonical-sig flag). - Assembler.PreSignXRP: pulls Sequence + suggested fee from rippled, embeds the release wallet's compressed-secp256k1 pubkey, returns the 32-byte SHA-512Half (via internal/xrpl) the MPC quorum signs. - Assembler.FinalizeXRP: parses (r, s) from the MPC's 65-byte R||S||V hex (ignores recovery byte — XRPL doesn't need ecrecover), DER-encodes with low-s canonicalization, attaches as TxnSignature, returns the wire blob (hex) + canonical XRPL tx hash. - XRPRequiredDrops: amount + fee + 10-XRP reserve, used by the gas pre-check to refuse signing when the wallet can't cover. - xrpFloatToDrops with the same string-format trick floatToWei uses, avoiding IEEE noise at 6-decimal native XRP precision. internal/mchain/client.go: - Wallet now carries ECDSAPubKeyHex (compressed-secp256k1 hex). EVM paths ignore it; XRP signing needs it on the wire. - pickAddress(XRP) derives the canonical r-address from the ECDSAPubKey via internal/xrpl.AddressFromPubKey instead of falling back to the ETH slot. Fallback retained for legacy mocks without the pubkey field. cmd/bridge/xrp_provider.go: - HTTP wrapper over rippled JSON-RPC (account_info, fee). Returns (Sequence, validated_ledger_index) and the open_ledger_fee/median fee / base_fee chain. Treats actNotFound on balance lookup as "wallet not activated → balance 0" so the gas pre-check correctly reports an unactivated release wallet. cmd/bridge/release_pool.go: - ReleasePoolEntry gains Family + PubKeyHex fields (round-trips through JSON; pre-XRP entries leave them empty). - AcquireForFamily filters entries by family — the signing driver now picks an XRP wallet for XRP destinations and an EVM wallet for EVM. Falls back to family-agnostic Acquire when no per-family match. - SizeByFamily + Reload helpers so a parallel XRP-pool bootstrap can share the same store as the EVM pool without re-indexing. cmd/bridge/signing_driver.go: - signOne dispatches on mchain.AddressTypeFor(DestinationNetwork). XRP routes to signOneXRP, which mirrors the EVM ceremony but: builds a Payment via Assembler.PreSignXRP, runs an XRP gas pre-check (balance vs amount+fee+reserve) instead of eth_getBalance, hands the SHA-512Half digest to the MPC, then Assembler.FinalizeXRP emits the signed wire blob. - xrpGasPrecheck short-circuits to SwapStatusFailedInsufficientReleaseGas with a humanized "balance=X drops, required=Y drops" reason. Probe failures DON'T block the swap (best-effort, broadcast layer still retries on transient rippled flakes). cmd/bridge/main.go: - New flags: --xrp-release-pool-size (default 5), --xrp-release-pool-mint-network (default XRP_TESTNET), --xrp-release-balance-threshold-drops (default 10 XRP). - XRPProviderClient instantiated once, shared between the assembler (for PreSignXRP) and the gas pre-check (via assembler.XRPProviderFor). - XRP pool minted on top of the existing EVM pool (via a helper-pool bound to the same store + Reload), so a single ReleasePool object serves both families. - /health surfaces pool sizes broken out by family. Tests: - xrpl: address derivation round-trip, bad-pubkey rejection, DER encoding + low-s flip, PreSign digest matches SHA512Half(STX||body), full PreSign→Finalize round-trip through the rubblelabs parser. - broadcast: tesSUCCESS happy path, terQUEUED retryable, tecINSUFFICIENT_FUNDS + tecNO_DST fatal, temBAD_AMOUNT malformed, telINSUF_FEE_P retryable, rippled high-level errors, classifyEngineResult table. - txassembler: PreSignXRP populates Sequence / Fee / LastLedger, fee floor + pin, bad-input rejection, FinalizeXRP round-trip, XRPRequiredDrops math, xrpFloatToDrops table. - mchain: XRP r-address derivation from ECDSAPubKey, legacy fallback to ETH slot, bad-hex rejection. - release pool: XRP-family Bootstrap + entry shape + PubKey roundtrip, AcquireForFamily filtering, no-match returns ErrEmptyPool. - signing driver: XRP happy path advances to broadcasting with populated DestRawTx, insufficient balance short-circuits, missing PubKeyHex rolls back with readable LastError. - xrp_provider: account_info / fee / balance stubs, actNotFound → 0, rippled error surfacing. |
||
|
|
9f398bdf30 |
feat(broadcast): BTC sendrawtransaction via mempool.space + retryable error surface
BTC broadcaster pushing serialized P2WPKH txs to mempool.space's
REST POST /api/tx (or operator's Bitcoin Core via RPCURLOverrides).
Family dispatch in Broadcast() now routes AddressTypeBTC to
broadcastBTC alongside the existing EVM path; SOL/TON/XRP/DOT still
return ErrFamilyNotImplemented for callers to detect.
Error surface — BTCBroadcastError carries:
- HTTPStatus from the upstream response
- Message (chain-side error string)
- Retryable bit so the orchestrator decides:
* retryable: leave swap at broadcasting (txn-already-known,
rate limit, 503/502/504, "already in mempool")
* fatal: advance to terminal failure (txn-mempool-conflict,
bad-txns-inputs-missingorspent, dust, missing inputs)
Special handling:
- "txn-already-known" / "Transaction already in block chain" → recover
the canonical txid locally from sha256d(raw) and surface as SUCCESS,
not failure (the tx is already where we wanted it)
- Upstream txid validation: 64-hex-char check; non-conforming bodies
fall back to local txid compute
Default URLs registered via init() in btc.go (not in client.go's
table) so the BTC defaults live next to the BTC broadcaster:
- BITCOIN_MAINNET → https://mempool.space/api/tx
- BITCOIN_TESTNET → https://mempool.space/testnet/api/tx
Tests: 13 across happy path (mainnet + testnet + 0x trimming),
already-known + already-in-chain success surface, mempool-conflict
+ missing-inputs fatal, 503/429 retryable, empty + malformed input,
malformed upstream body fallback, error stringification with
retryability label
|
||
|
|
e84b531b5b |
feat(txassembler): BTC P2WPKH build + BIP143 sighash + greedy UTXO selection
Wire-correct BTC release tx assembler that the MPC quorum can threshold-sign and the broadcast driver can push. Mirrors the EVM PreSign/Finalize shape but emits N per-input sighashes (one MPC sign call per input) instead of one EIP-155 digest. Scope (v1): - P2WPKH witness-v0 only (bech32 bc1q.../tb1q...); P2TR deferred - SIGHASH_ALL only (no anyonecanpay / single) - Greedy largest-first UTXO selection — adequate for the bridge's own pool wallet, no churn concerns - Single change output back to the release address (dropped under the 546 sat dust threshold) - BIP143 sighash via btcd/txscript.CalcWitnessSigHash; scriptCode computed from the P2WPKH pkScript via the canonical OP_DUP OP_HASH160 <pkh> OP_EQUALVERIFY OP_CHECKSIG transform - Fee estimation: mempool.space halfHourFee with fastestFee / minimumFee fallback; static fallback rate 10 sat/vB - Low-s canonicalization in Finalize (BIP146); high-s sigs are refused by relay nodes - DER encoding: canonical with 0x00 leading-pad when high bit set - RBF-friendly sequence (0xfffffffd) on every input What's NOT in this commit: - PSBT wire format — we use raw wire internally; PSBT is a hardware-wallet convenience irrelevant in-process - Bitcoin Core RPC — operators can override broadcast URL but the default uses mempool.space's REST API (zero-setup) New types: - BTCNetwork, BTCSpec, BTCUTXO, BTCInputCtx, BTCUnsigned, BTCECDSASig - BTCAssembler with UTXOFetcher / FeeEstimator / BalanceProbe seams - MempoolSpaceClient implements all three from public REST endpoints Tests: 30+ across selectBTCUTXOs (greedy correctness, insufficient funds, fee scaling), p2wpkhScriptCode, chainHashFromTxID round-trip, encodeDERSignature (low-s + high-bit padding + zero rejection), ParseBTCRSV, PreSign happy path + dust handling + insufficient funds + pubkey/address validation, Finalize witness layout + sig count mismatch, MempoolSpaceClient UTXO/fee/balance shape parsing |
||
|
|
0e1ad73ace |
feat(mchain): BTC P2WPKH derivation from keygen ECDSAPubKey
Extend the MPC keygen client to surface the compressed secp256k1 public key alongside the chain-appropriate address, and derive a bech32 P2WPKH locally for BITCOIN_* networks instead of relying on mpcd's legacy P2PKH "1..." output. Why: bridge release on BTC needs P2WPKH (witness-discount fees, direct btcsuite/txscript compatibility, broader wallet support). mpcd's pubKeyToBtcAddress emits Base58Check P2PKH; rather than round-tripping a fix through mpcd, the bridge re-derives locally from the same ECDSAPubKey the keygen already returns. Changes: - Wallet gains ECDSAPubKey []byte; 33-byte compressed form preferred, uncompressed (65/64) and x-only (32) inputs all compress canonically - keygenResult parses both eth_address and evm_address (mpcd renamed) - pickAddress() takes the network internal_name so BITCOIN_MAINNET vs BITCOIN_TESTNET selects the right bech32 hrp (bc / tb) - new btc_address.go: deriveBTCBech32Address + compressSecp256k1Pubkey + bitcoinChainParams; uses btcd/btcutil for canonical HASH160 + segwit-v0 encoding - 11 new tests covering mainnet/testnet hrp selection, 32/33/64/65 byte pubkey shapes, empty/malformed input, keygen integration |
||
|
|
c96474d7ff |
feat(bridge): wire TON end-to-end through mchain + pool + signing driver
internal/mchain: - pickAddress for AddressTypeTON now derives the real v4r2 address from EDDSAPubKey when the keygen surfaces it (workchain=0 + DefaultSubwallet=698983191 + V4R2 code + pubkey → state_init.Hash). Falls back to the legacy SOL slot when the cluster hasn't yet emitted the Ed25519 pubkey, so existing mpcd deployments don't break. - mchain.Wallet now carries ECDSAPubKey + EDDSAPubKey so downstream consumers (release_pool, signing_driver) can drive chain-specific flows without re-querying mpcd. - New ton.go houses deriveTONAddressFromHex, delegating cell encoding to tonutils-go (same lib the txassembler depends on) so addresses surfaced at keygen time are bit-identical to those the txassembler rebuilds in state_init at first send. cmd/bridge/release_pool.go: - ReleasePoolEntry gains AddressType + ECDSAPubKey + EDDSAPubKey fields so the signing driver can route per-family. Legacy entries without these fields fall through to the EVM path (AddressType= empty equals "eth" via the dispatch default). cmd/bridge/balance_probe.go: - TON branch added to BalanceAt: GET <toncenter>/getAddressBalance, optional X-API-Key header from c.TONAPIKeys[network]. Returns *big.Int of nanoton. Same best-effort policy as the EVM path (probe failures log and skip rather than block signing). - TONBalanceURLs + TONAPIKeys maps populated by main.go from networks.yaml's per-network Broadcast block. cmd/bridge/signing_driver.go: - New tonAssembler field + SetTONAssembler setter mirroring SetAssembler. Dispatch in signOne now branches on mchain.AddressTypeFor(DestinationNetwork): TON → PreSignTON path (looks up the pool entry's EDDSAPubKey, builds the v4r2 unsigned body, computes the nanoton-unit gas pre-check, finalizes via FinalizeTONHex into base64 BOC); otherwise → EVM path (unchanged). - gasPrecheckTON: 0.05 TON gas floor for native transfers, 0.15 TON for jetton transfers (covers body value + forward TON + master call gas). Native transfers count the user amount against the release wallet; jetton transfers don't (the user value moves between jetton wallets, not out of ours). - tonAmountToNano + isTONNativeAsset helpers — same fixed-point float→big.Int approach as the EVM floatToWei to avoid IEEE noise. cmd/bridge/config.go: - Network gains an optional Broadcast block (URL + APIKey) so operators configure TON Center endpoints per-network in networks.yaml without flag plumbing. cmd/bridge/main.go: - TON assembler instantiated at startup and wired into the signing driver via SetTONAssembler. - Broadcast client picks up per-network sendBoc URLs + X-API-Keys from the new networks.yaml block. - Balance probe picks up per-network getAddressBalance URLs (derived from sendBoc by path-swap) + the same X-API-Keys. |
||
|
|
84d4d56cf4 |
feat(mchain): derive substrate SS58 client-side; deprecate ErrSubstrateNotImplemented
The bridge previously fast-failed DOT keygen with ErrSubstrateNotImplemented. The MPC cluster already returns a 33-byte compressed secp256k1 pubkey (ecdsa_pub_key) — the only missing piece was the SS58 derivation, which internal/substrate now provides. Changes: - Wallet.ECDSAPubKey: new field carrying the compressed pubkey hex. Required by the DOT signing path so the assembler can derive the AccountId and resolve the ECDSA recovery byte at Finalize. - pickAddress: AddressTypeDOT now derives SS58 from ECDSAPubKey via blake2_256(pub) -> AccountId32 -> base58check with the per-network prefix (mainnet=0, Westend=42, Kusama=2). Returns ErrMissingPubKey if the cluster forgot to populate ecdsa_pub_key. - networkAddressType: add POLKADOT_TESTNET, KUSAMA_MAINNET as DOT family (alongside the existing POLKADOT_MAINNET entry). - ErrSubstrateNotImplemented: kept exported (callers may errors.Is) but documented as deprecated. KeygenForDeposit no longer returns it. Tests updated: - TestKeygen_DOT_DerivesSS58FromPubKey: pin a deterministic pubkey, verify the SS58 address starts with '1' (Polkadot prefix 0). - TestKeygen_DOT_TestnetPrefix: Westend (prefix 42) addresses start with '5'. - TestKeygen_DOT_MissingPubKeyErrors: empty ecdsa_pub_key -> ErrMissingPubKey. - cmd/bridge swap handler test: TestSwapsCreate_DOTRequest_Succeeds replaces the old "returns 501" expectation. |
||
|
|
4a75d55b8d |
feat(broadcast): XRP submit via rippled JSON-RPC
internal/broadcast/xrp.go: XRP-family broadcaster targeting rippled's
`submit` method.
- Wire: POST {method:"submit", params:[{tx_blob:"<hex>"}]} to
https://s1.ripple.com:51234/ (mainnet) or
https://s.altnet.rippletest.net:51234/ (testnet). Strips a 0x prefix
if the caller (every other broadcaster in this package emits 0x...)
appends one — rippled rejects 0x-prefixed blobs.
- classifyEngineResult: maps rippled's engine_result codes to one of
success / retryable / fatal:
tesSUCCESS → success → return result.tx_json.hash
ter* → retryable (broadcast driver re-submits)
tel* → retryable (local server, may resolve next round)
tec* / tem* / tef* → fatal (consumed-fee failure, malformed, or
server-rejected past-seq / already / etc.)
unknown / empty → retryable (rippled wasn't sure yet)
Behaviour matches xrpl.org/transaction-results.html.
- doPlainJSON helper added to client.go: rippled is JSON-RPC-shaped
(method, params) but does NOT emit the {jsonrpc,id,result,error}
2.0 envelope, so it needs its own POST/decode helper.
- Broadcast() dispatch grows an AddressTypeXRP arm.
- rpcURLs table grows XRP_MAINNET + XRP_TESTNET defaults; operators
shadow per-env via --source-rpc-overrides.
Tests: tesSUCCESS happy path (returns tx_json.hash), terQUEUED
retryable, tecINSUFFICIENT_FUNDS + tecNO_DST fatal, temBAD_AMOUNT
malformed fatal, telINSUF_FEE_P retryable, high-level rippled error
surfaced as RPCError, network transport error, success-without-hash
rejection, 0x-prefix stripping, classifyEngineResult table.
|
||
|
|
bdfadf528c |
feat(txassembler): Polkadot ECDSA extrinsic builder + Finalize
Add internal/txassembler/dot.go — the DOT analogue of the EVM Assembler.
Pipeline:
1. DOTSpec ── PreSign ──> DOTUnsigned (call bytes + signing payload)
2. DOTUnsigned + (r,s,v) ── Finalize ──> raw_extrinsic_hex + extrinsic_hash
PreSign behaviour:
- decode recipient SS58, refuse cross-prefix transfers (operator
typo'ing a mainnet → testnet address surfaces loudly).
- SCALE-encode balances.transfer_keep_alive(MultiAddress::Id(dest),
Compact<value>) using the per-network CallIndex.
- derive signer AccountId32 from compressed pubkey via
blake2_256(pub) — substrate ECDSA convention.
- build ExtrinsicPayloadV4 = call || era(immortal) || nonce ||
tip || spec_version || tx_version || genesis_hash || block_hash;
blake2_256-hash it if it exceeds 256 bytes.
Finalize behaviour:
- canonicalize low-s and (if hint was definite) flip v.
- If recoveryHint ∈ {0,1}: try only that v.
If 0xff: try both v=0 and v=1, pick the one whose recovered
pubkey verifies the signature against the signer pubkey.
Neither matching → "sign context wrong" (the MPC signed the
wrong payload, or the wrong key share was used).
- Wrap in MultiSignature::Ecdsa (tag 0x02) || 65-byte (r||s||v).
- Assemble v4 signed extrinsic via substrate.AssembleSignedExtrinsic.
dot_crypto.go: self-contained pure-Go secp256k1 ECDSA verify (math/big
only). About 100 lines: curve params, affine point arithmetic,
compressed-pubkey decoding via y = x3^((P+1)/4), standard ECDSA verify.
No CGO; runs cross-arch.
Tests cover PreSign happy-path with golden call bytes, prefix-mismatch
refusal, bad-input rejection, Finalize round-trip with both an explicit
and an auto-discovered recovery byte, wrong-signer-fails-both,
ParseDOTSignature normalization (27/28 → 0/1, unknown → 0xff), and the
ECDSA verify self-check against known scalars.
|
||
|
|
61d403336e |
test(bridge): TON broadcast + v4r2 assembler test coverage
internal/broadcast/ton_test.go:
- Happy path: hex payload → base64 on wire, X-API-Key header set
per-network, response hash returned verbatim.
- Base64 payload pass-through (skip the hex re-encode).
- Error classification matrix:
429 / "rate" → ErrTONRateLimited (retryable)
"invalid boc" / "wrong signature" → ErrTONMalformedBOC (fatal)
"account not initialized" → ErrTONAccountNotInitialized (re-build w/ state_init)
HTTP 502/503/504 → TONError{Retryable:true}
- Family-dispatch parity: TON_TESTNET must NOT trip ErrFamilyNotImplemented.
- Direct parser tests (no HTTP) for non-JSON 5xx + hash-less success
payloads (synthesized "ton:sendBoc:ok:200" identifier).
- isHexString + payload normalizer unit tests.
internal/txassembler/ton_test.go:
- Address derivation: same pubkey ⇒ same address (deterministic),
different pubkeys ⇒ different addresses, UQ vs EQ flags surface
correctly via TONAddressFromPubKey / …BounceableFromPubKey, raw
form parses back to the same address (parse-roundtrip via
ParseTONAddress).
- PreSign native transfer: deterministic body hash under a fixed
clock, StateInit populated/skipped per SourceInitialized flag,
rejects nil / zero / negative amounts + bad pubkey + spec
SourceAddress mismatch.
- PreSign jetton: spec-provided JettonSourceWallet skips the
JettonProvider, provider path is invoked with the right
(network, master, owner) args, missing-provider + missing-master
both error cleanly.
- PreSign + FinalizeTONHex round-trip: locally sign u.SigHash with
Ed25519, finalize, decode the resulting BOC, verify
ExternalMessage.Body's first 64 bytes match the signature.
- Body schema: 1 ref (the internal message), 112 bits of header
data (subwallet 32 + valid_until 32 + seqno 32 + op 8 + mode 8).
- State_init hash matches the pubkey-derived address — confirms
the wallet contract we deploy on first send IS the wallet
contract whose address we keygen'd to.
- Static seqno provider drives v4r2 body's seqno field.
|
||
|
|
5c4ae63411 |
feat(xrpl): Payment codec + address derivation + DER low-s signature
Introduces internal/xrpl, the bridge's XRP Ledger primitives package: - AddressFromPubKey / AccountIDFromPubKey: derive the canonical r-address from a 33-byte compressed-secp256k1 public key. Hash chain is RIPEMD160(SHA256(pubkey)), base58check-encoded with the XRPL alphabet + 0x00 version byte. Round-trips through the rubblelabs codec. - PreSign(PaymentUnsigned) → (32-byte digest, *SignCtx): builds a native XRP Payment, embeds the signing pubkey on the wire, returns the SHA-512Half of (0x53545800 || serialized_body) — exactly the message the MPC quorum must sign. - Finalize(DER, *SignCtx) → (signedBlob, txHash): attaches the DER signature to TxnSignature, returns the full wire-format binary blob (suitable for submit's tx_blob param as hex) and the canonical XRPL tx hash (SHA-512Half of 0x54584E00 || blob). - EncodeDERSignature(r, s): ASN.1 DER encode (r, s) with low-s normalization (rejects high-s as XRPL's tfFullyCanonicalSig requires) and the 0x00 leading-byte rule for INTEGERs with the MSB set. - CanonicalizeS / SHA512Half exposed for tests + future TrustSet path. Uses github.com/rubblelabs/ripple for the deep binary codec — every field-id, type-byte, and length-prefix detail is the library's responsibility. The bridge owns only the signing flow on top, so MPC ECDSA can stand in for the library's local-key Sign helper. Tests cover: canonical pubkey → r-address round-trip, rejection of bad pubkey lengths/prefixes, DER encoding of standard and high-bit cases, low-s flip for s > N/2, validation that high-s gets canonicalized in EncodeDERSignature, PreSign digest matching independent SHA512Half recomputation, full PreSign → Finalize round-trip via the parser. |
||
|
|
73aaa949fb |
feat(bridge): TON broadcast + v4r2 wallet tx assembler
Add the wire-correct TON destination handler so the bridge can release
into TON_MAINNET / TON_TESTNET via TON Center.
internal/broadcast/ton.go:
- broadcastTON pushes a signed external-message BOC to TON Center's
sendBoc endpoint (mainnet: toncenter.com/api/v2/sendBoc; testnet:
testnet.toncenter.com/api/v2/sendBoc).
- X-API-Key header sourced per-network from Client.TONAPIKeys (set via
SetTONAPIKey or populated by main.go from networks.yaml). Required
at production rates — public gateway throttles to ~1 req/s.
- normalizeTONPayload accepts the hex shape the txassembler emits (to
match the EVM raw-tx convention) and converts to base64 for the
toncenter wire. Pre-base64 payloads pass through.
- parseTONResponse classifies error responses: 429 / "rate" →
ErrTONRateLimited, "account not initialized" / "state_init" →
ErrTONAccountNotInitialized, "invalid boc" / "wrong signature" →
ErrTONMalformedBOC (fatal — signer must re-build), other 5xx /
transport → TONError{Retryable: true}.
- Returns the in-message hash as BroadcastResult.TxHash. Callers
resolve the on-chain tx hash later via getTransactions polling.
internal/txassembler/ton.go:
- Two-phase PreSign / Finalize parallel to the EVM path. PreSignTON
builds the wallet-v4r2 unsigned body cell (subwallet || valid_until
|| seqno || op=0 || message_mode || ref_to_internal_msg) and
surfaces its representation hash for the MPC Ed25519 quorum to
sign. FinalizeTON / FinalizeTONHex re-assemble with the 64-byte
signature, wrap in an external-in message (with state_init when
SourceInitialized=false so first sends self-deploy), and emit BOC
bytes (or base64) for the broadcast layer.
- Native TON transfer + jetton transfer (TRC-3 op=0x0f8a7ea5) under
one spec. Jetton path resolves the source's jetton-wallet address
off the master either via TONJettonWalletAddressProvider (live
runMethod) or operator-supplied TONSpec.JettonSourceWallet. Forward
TON convention 0.05 TON (50_000_000 nanoton) for gas — same
amount the body itself carries to the master.
- Wallet v4r2 (= tonwallet.V4R2) + sub-wallet 698983191 (=
tonwallet.DefaultSubwallet). messages_ttl=180s matches upstream.
- Address helpers: TONAddressFromPubKey (UQ), …Bounceable… (EQ),
TONRawAddress ("<workchain>:<hex>"), ParseTONAddress (accepts any
form). Exported so mchain.pickAddress can derive a real TON
address from EDDSAPubKey once mpcd surfaces it, and so the
deposit-watcher can match incoming-deposit addresses.
internal/broadcast/client.go:
- Add TONAPIKeys map on Client + AddressTypeTON dispatch case
routing to broadcastTON. ErrFamilyNotImplemented message updated
(BTC/SOL/XRP/DOT remain not implemented; EVM + TON are in).
|
||
|
|
dd4a5e3d61 |
feat(bridge): SOL broadcaster, lamport balance probe, per-family pools
Three layered changes to drive Solana to the broadcast leg of the pipeline:
1. internal/broadcast/sol.go — sendTransaction over JSON-RPC.
broadcastSOL packs the base64-encoded signed tx that the assembler
produced into a Solana sendTransaction call with
{encoding:"base64", preflightCommitment:"confirmed", skipPreflight:false}.
preflightCommitment matches the assembler's blockhash commitment so
the cluster's preflight simulation runs against a consistent
snapshot.
Two typed errors mirror the canonical upstream messages:
- SOLBlockhashNotFoundError — Retryable() == true. The signing
driver should drop the current message, rebuild PreSign with a
fresh blockhash, re-run the MPC ceremony.
- SOLSimulationError — Retryable() == false. Deterministic failure
(insufficient SPL balance, malformed instruction, missing ATA);
same MPC ceremony won't fix it. Logs extracted from error.data
are surfaced for operator triage.
Anything else falls through to the existing *RPCError shape.
client.go: register SOLANA_{MAINNET,DEVNET,TESTNET} in rpcURLs +
dispatch AddressTypeSOL → broadcastSOL.
2. balance_probe.go — getBalance for Solana (lamports).
BalanceAt now branches on network family:
- EVM: eth_getBalance, returns wei
- SOL: getBalance with commitment=confirmed, returns lamports
- Other non-EVM (BTC/TON/XRP/DOT): ErrFamilyNotSupportedForBalance,
gas pre-check skipped (preserves existing behaviour)
The new doRPC helper extracts shared HTTP transport so the EVM and
SOL paths don't duplicate boilerplate. Lamport result decoded
through json.Number → *big.Int for safety even though it always
fits in uint64.
3. cmd/bridge/release_pool_set.go + release_pool_family_store.go —
per-family release-wallet pool routing.
The existing *ReleasePool is single-family by design (one mint
network, one rotation cursor). Multi-family deployments need
independent pools per family because the keygen call, address
format, balance unit, and funding flow are all family-scoped.
ReleasePoolSet wraps {EVM, SOL, BTC} *ReleasePool pointers and
dispatches Acquire by mchain.AddressTypeFor(destinationNetwork). A
nil per-family pointer surfaces ErrNoPoolForFamily so the signing
driver falls back to the deposit-as-release legacy path.
Family-scoped persistence (ZapStoreFamily / InMemoryStoreFamily)
keys entries under releasepool:<family>:NNNNNN. The legacy
releasepool:NNNNNN namespace stays intact for backward compat with
pre-multi-family deployments. FamilyEVM/SOL/BTC constants are
wire-stable (changing them breaks reload of existing pools).
The InMemoryStore now holds an extra familyPools map alongside the
single-pool slot — the legacy pool path stays exactly as it was;
multi-family layers in.
Tests:
- internal/broadcast/sol_test.go: happy path, malformed response,
BlockhashNotFound retryable classification, SimulationError fatal
classification + log extraction, empty raw tx, empty result,
classifySOLError direct unit coverage, RPC URL table presence.
- cmd/bridge/release_pool_set_test.go: family routing (EVM vs SOL),
per-family round-robin isolation, Size + FamilySizes diagnostics,
InMemoryStoreFamily namespace isolation + ascending-index iter,
FamilyCode constant pinning.
All package tests green:
ok cmd/bridge 3.957s
ok internal/broadcast (cached)
ok internal/mchain (cached)
ok internal/txassembler (cached)
|
||
|
|
3663252fd8 |
feat(broadcast): wire Polkadot author_submitExtrinsic + error classification
Add the substrate broadcast handler at internal/broadcast/dot.go:
- POST {"method":"author_submitExtrinsic","params":["0x<hex>"]} against
the network's substrate RPC, return the 0x-prefixed extrinsic hash
on success.
- DOTBroadcastError classification (retryable vs fatal) by substrate
error code + message:
Invalid::Future / AlreadyImported / pool-full / runtime-upgrade
-> retryable (orchestrator polls again next tick)
Invalid::Stale / Invalid::BadProof / Module(...)
-> fatal (re-sign needed or pallet rejection)
- Default RPC endpoints: rpc.polkadot.io (mainnet),
westend-rpc.polkadot.io (testnet), kusama-rpc.polkadot.io (canary).
Wire dispatch in client.go Broadcast switch: AddressTypeDOT lands in
broadcastDOT. ErrFamilyNotImplemented now reads BTC/SOL/TON/XRP only.
Tests cover happy-path, Stale/BadProof fatal, Future retryable,
AlreadyImported retryable, Module error fatal, 4xx-with-envelope, 5xx
without envelope, empty result, and error-string formatting.
|
||
|
|
4dfb24878c |
feat(txassembler): SOL message build + signature finalize
Adds SOLAssembler — companion to the EVM Assembler — that constructs
Solana transactions for the bridge's destination-release flow:
- PreSign: fetch a recent blockhash via JSON-RPC getLatestBlockhash,
build a v0 message (one signer = the MPC release wallet as fee
payer, one recipient, lamport amount), serialize the bytes to be
signed. SPL token mode (when SourceMint is set) derives source +
destination ATAs via solana.FindAssociatedTokenAddress and probes
the destination ATA via getAccountInfo — if absent, the assembler
prepends a CreateAssociatedTokenAccount instruction so the release
wallet pays the ~2_039_280 lamport rent + creates the account.
- Finalize: slot the 64-byte Ed25519 signature into Transaction
.Signatures[0], serialize the full wire tx with MarshalBinary, and
base64-encode for JSON-RPC sendTransaction (encoding:base64). The
signature itself doubles as the canonical Solana "tx hash" — return
it base58-encoded for SDK consumption.
Dependency: github.com/gagliardetto/solana-go v1.20.0 — the de facto
standard Go client. The assembler stays decoupled from the broader
*rpc.Client beyond GetLatestBlockhash + GetAccountInfo, so the
broadcast layer can use its own JSON-RPC endpoint.
Helpers:
- LamportsFromFloat(amount, decimals): safe float → uint64 conversion
with overflow protection. 9dp for native SOL; varies for SPL.
- ParseSOLSignatureHex(s): decodes a 128-char hex string to a fixed
[64]byte buffer — used by the signing driver when the dashboard
returns the Ed25519 sig in hex form.
- SOLDefaultBlockhashURL(network): canonical RPC URL per network.
Tests (sol_test.go) cover:
- Native transfer happy path: instruction is system.Transfer at
Account[0] = payer; round-trip of MessageBytes through
Message.Unmarshal yields an identical message.
- SPL transfer with destination ATA missing: 2 instructions
(ata.Create, then token.Transfer), destination ATA correctly
derived and probed.
- SPL transfer with destination ATA present: only 1 instruction.
- Finalize round-trip: rawTx parses back through solana-go's
Transaction decoder with matching signature + payer + blockhash.
- Error paths: unknown network, zero amount, malformed payer addr.
- ParseSOLSignatureHex + LamportsFromFloat: corner cases.
Total: 12 SOL tests, all green.
|
||
|
|
a560468888 |
feat(substrate): SCALE codec, SS58 encoder, signed extrinsic builder
Add internal/substrate package containing the minimal Polkadot wire
primitives the bridge needs to sign + broadcast balances.transfer_keep_alive
extrinsics:
- SCALE codec primitives (compact ints, fixed-width LE ints, byte slices
bare + length-prefixed) — enough to encode ExtrinsicPayloadV4 + a v4
signed extrinsic. Not a general reflection-driven encoder.
- AccountId32 derivation from a compressed secp256k1 pubkey via
blake2_256 — matches sp_io::hashing::blake2_256 on the runtime side.
- SS58 base58check encoder + decoder with single-byte network prefixes
(Polkadot=0, Kusama=2, generic=42). Verified against the canonical
Alice fixture in both Polkadot mainnet ("15oF4u...") and substrate
generic ("5Grwva...") forms.
- ExtrinsicPayloadV4 builder: SCALE(call) || era || nonce || tip ||
spec_version || tx_version || genesis_hash || block_hash, with the
>256-byte → blake2_256 hashing rule.
- v4 signed-extrinsic assembler: 0x84 version byte || MultiAddress::Id
signer (33B) || MultiSignature(scheme, sig) || era || nonce || tip ||
call, length-prefixed compact-style.
The substrate primitives are leaf code with no network I/O. Test vectors
are pinned against the Polkadot JS SDK and substrate runtime references.
|
||
|
|
83f854f160 |
feat(mchain): per-curve keygen + sign — Ed25519 hint for SOL/TON/DOT
The lux-mpc cluster supports both ECDSA/secp256k1 and EdDSA/Ed25519 (FROST) keygens. The bridge previously hardcoded secp256k1 because every active swap path was EVM/BTC. To wire Solana end-to-end we need the cluster to mint an Ed25519 wallet and sign over raw Solana message bytes — neither of which work under secp256k1. Add a Curve type with two members (secp256k1, ed25519) and a CurveFor(AddressType) mapping. Forward the curve hint on both the keygen and dashboard /v1/mpc/sign request bodies; older daemons that don't recognize the field fall back to their compile-time default (secp256k1) — which produces an ECDSA-shaped signature the SOL finalizer will reject with a clear error, so a deployment mismatch fails fast instead of silently producing bad release txs. SignForWallet and SignForWalletWithOrg stay on secp256k1 (default behaviour preserved). The new SignForWalletOnCurve/OnCurveWithOrg entry points let the signing driver pass an Ed25519 hint per swap. Tests: - TestKeygen_SOL_SendsEd25519Curve: SOL keygen body has curve=ed25519 - TestKeygen_ETH_SendsSecp256k1Curve: ETH stays on secp256k1 - TestCurveFor: coverage across all six AddressTypes |
||
|
|
78ff44fe9d |
feat(mchain): wire SignForWallet to dashboard /v1/mpc/sign (real JWT/API-key path)
The live lux-mpc v2026-05 daemon does NOT expose /sign on the internal
port (:6000); only /keygen lives there. Signing is on the dashboard
listener (port 8081) at POST /v1/mpc/sign with {message, encoding,
walletId, sessionId, value} returning {signature, r, s}. That endpoint
requires a pre-created session minted by POST /v1/mpc/wallets/{id}/sessions.
This commit:
- Adds DashboardURL + DashboardToken (Bearer) + DashboardAPIKey
(X-API-Key) fields to mchain.Client. Bearer wins when both are set.
- Adds internal/mchain/auth.go with EnsureDashboardSession() that
lazily mints + caches one signing grant per (walletID, orgID) per
process. Sessions live 24h with a 15-minute renewal skew.
- signViaDashboard() handles POST /v1/mpc/sign, including a single
403-retry path that drops the cached session and re-mints. The
dashboard sometimes returns only {r,s}; the client composes a
canonical 65-byte EVM signature with v=27 (the assembler's
ParseRSV tolerates the parity bit and any malleability would be
caught at broadcast).
- pending_approval sessions are rejected as a hard error (the bridge
cannot wait for a human approver mid-swap).
- SignForWalletWithOrg now routes through the dashboard when
DashboardURL is set; APIURL-only configs keep the legacy /sign
path for in-process mocks.
- New auth_test.go covers happy path, session caching across signs,
403-triggered renew, Bearer vs API-key precedence, pending_approval,
HTTP error surfacing, and r+s composition with zero-padding.
Builds on whispers/bridgev2 @
|
||
|
|
21020685d9 | feat: refunding has been working | ||
|
|
a35e775173 | feat: simple base swap is working now | ||
|
|
b1241c9fda | feat: added mpc sign option |