[V136] feat: add Solana (SOL)
Summary
Part 7 of 7, the last branch of the V136 chain stack (tracking issue #325). This MR targets v136/05-tron, so the diff shows only this branch's commits. Review those commits only; the branches below it have their own MRs.
It adds Solana (SOL, gas asset SOL.SOL) as a new chain, active from network version 1.136.0. Before activation, every SOL case in common is behind SOLChain.IsActivated() and bifrost does not scan, sign or post for SOL (activation gate from v136/00-chain-activation).
Size: 117 files, +15 609 / -291, plus 59 regression export files under test/regression/mnt.
Changes
Common ([V136])
5504180a1:SOLChain,SOL.SOL(no short code), gated predicates,IsValidSOLAddress, a program/sysvar recipient blacklist,PubKey.GetAddress(SOL)(ed25519 keys only),NewTxIDV136(canonical base58 of 64 bytes, 86 to 88 characters, case preserved).9b0ac9c59: a checksum-valid bech32/bech32m string is never a SOL address. About 8% ofmaya1…/thor1…addresses also decode to 32 bytes of base58.a54b4cae5:ApproximateBlockMilliseconds(SOL)= 300. Only queries and the SOL scanner use it.6a2677c29:FuzzNewAddressV136over every V136 chain.
State machine ([V136])
6f98d4b53: the memo parser readsOUT:/REFUND:tx ids withNewTxIDV136from 1.136.0 and withNewTxIDbefore. Without the gate, a 1.135 binary would refund what a 1.136 binary treats as an outbound. Stored-hash re-parsers, querier and CLI useNewTxIDV136.91f67ef63: SOL inSUPPORT_CHAINS_VCURRENT(mainnet, stagenet, mocknet).014384470: archiveTxOutStorageV133(manager_txout_v133.go) andNetworkMgrV135(manager_network_v135.go). Byte copies, type renamed.1.136.0returns VCUR.e4967ea1a: TxOut and network VCUR:- an outbound memo base is never truncated; if it does not fit, the outbound fails and the caller refunds;
- every vault keeps the SOL rent reserve on every SOL outbound; Ragnarok leaves a bounded residual;
- SOL outputs below dust are dropped on every path;
- a SOL destination that is a case variant of a vault address is refused;
- a SOL migration keeps the reserve and burns it from the books in the next round (a balance that cannot be migrated is burned after the final round, see Review follow-up).
6489120b7: exact SOL address match in the observation filter.ObservedTxIn/ObservedTxOuthandleV136compare SOL self-sends exactly. The txin one builds onhandleV135, the txout one onhandleV133. The old handlers move unchanged to the archive files.495d6bc31: swap quote refuses SOL to BTC/DASH/transparent ZEC.OUT:<sig>is up to 92 bytes and does not fit their 80-byte memo.18172aa4b(test only): SOL observations failValidateBasic(code 6) before 1.136.0 through the activation gate, and pass from 1.136.0.
Bifrost ([Version-unspecific])
158a7019a: plumbing. BridgeGetBlockTimestamp, LevelDB SOL cursor, exact SOL match in pubkey manager and observer,NewTxIDV136in the observer, solvency, metrics, config (disabled: true).0d2c60682:bifrost/pkg/chainclients/solanaclient, scanner and RPC.- Parity fixes (see below):
2a65a31e9,58075b005,d6c427432,811bfff41,7543fe215.
Test, build, docs
dbbd5d2da: mocknet simulation wiring (test/simulation/pkg/solana, stdlib ed25519, no SDK).09ccf5d4b: mocknet Agave test validator, pinned by digest.db3361d7b:docs/chains/solana.md.646e1a47a: regression suitessuites/sol/{swap,refund-sol-to-btc,version-gate}.yaml,sol_txid/sol_txid_86templates.42bcb0c14: regenerated exports.
Versioning and consensus impact
- New V136 code:
NewTxIDV136and its memo gate, SOL cases in the V136 address set, TxOut and network VCUR (from 1.136.0), observed-txhandleV136andpreflightV136, the exact-case voter functions (Tx.EqualsExV136,ObservedTxVoter.AddV136/GetTxV136). - Frozen bodies moved unchanged:
TxOutStorageV133,NetworkMgrV135, observed txinhandleV135/handleV133/handleV127/preflightV135, observed txouthandleV133/preflightV135. lint-versionspasses;#check-lint-warningis not needed.9b0ac9c59anda54b4cae5change V136 functions in place. V136 is unreleased.- Not changed: archived memo parsers, the frozen slasher
REFUND:re-parse (falls back to the TxOutItemInHash), the order book manager (not on any block path).
THORNode references and parity
Ported from the THORNode SOL client at c3d15b5dd, then compared with THORNode develop cd6c065108. Every baseline fix is present. Findings fixed on this branch:
| Finding | Commit | THOR reference |
|---|---|---|
| D1: a page budget overrun wedged a vault forever | 58075b005: page checkpoints, budget is per pass |
THOR has no cap |
D2: maxSupportedTransactionVersion: 0 |
2a65a31e9: version 1, skip -32015 with a metric |
7c297420a |
| D3: an unhealthy vault could be reported insolvent | d6c427432: GetVaultAccount errors for that vault |
befc3af4e4 (mainnet incident) |
| E3: outbounds to program accounts looped forever | 811bfff41: getAccountInfo check before keysign, fail open |
sol_presign_filter.go (4282a76fe) |
| E4: block time 400 ms | a54b4cae5: 300 ms |
THOR common/chain.go |
E5: unguarded RPCVersion, no address fuzz |
7543fe215 (dead type removed), 6a2677c29 |
88b77378ab |
| S1: bech32 strings accepted as SOL (also in THOR) | 9b0ac9c59 |
— |
Kept on purpose (MAYA fixes THOR does not have): exact case-sensitive vault matching, null getTransaction holds the cursor, 86-character signatures, no memo truncation, rent reserve on every outbound, strict inbound parsing, pinned keysign, stricter signer-cache reconciliation. C1 to C5 are worth reporting upstream.
Not ported on purpose: keep-alive self-sign, instant observation from SignTx, ScanInactiveVaults, per-chain floor block height. Decimals stay 8; pool decimals 9 come from the coin.
thornode!5041 and thornode!5128 (SOL signer-cache follow-ups to thornode!4770) do not apply: this client uses a block-height expiry (lastValidBlockHeight + 32) with a finalized-status check, not the wall-clock TTL.
Tests
- Unit:
make testandmake test-network-specificpass. Coverage includes the activation rule (every predicate equalsChain("ZZZ")at 1.135.0), the memo gate, rent reserve on split outbounds, Ragnarok and migration reserve, case-variant refusal, prefix collisions, the bech32 collision, page checkpoints, unsupported tx versions, solvency with an unhealthy vault, and the recipient check. - Fuzz:
FuzzNewAddressV136; a 30 s run is clean. Seeds only are committed. - Regression: full run with
EXPORT=1: 153/153 suites, 0 failures. Exports unchanged after the parity fixes, and again after review rounds 3 and 4 (rerun on the restacked branch). - Mocknet: full static simulation passes (279 actors, run 2). Run on 2026-10-19. It ran before the THOR-parity fixes; the regression run above includes them. This covers DualLP, insolvency, 23 SOL swap routes and the Ragnarok forced path. Run 1 failed on a
feature-Consolidate-ZECtimeout, a load flake that passes on 03, 04 and 05. - Lint: gofumpt, handlers, managers, analyze, versions.
Review follow-up
zlyzol's second review of this MR. Each fix has a test that fails on the old code.
1b4a241f6[V136]: SOL base58 has no checksum, so a case variant of a SOL address or tx id is another valid value. The voter grouped and tallied votes case-insensitively. One signer could make a case-changed SOL sender the consensus tx, or put a case-changed SOL destination in the memo of the vote that reaches quorum, also on an inbound from another chain. From 1.136.0, SOL ids and addresses are compared exactly, and the memo exactly on every chain, in vote grouping and in the quorum tally (txin and txout). Older versions are unchanged.d2871760a[V136]: a retiring vault holding between reserve + gas and 2 × reserve + gas (for example 0.0015 SOL) was neither migrated nor burned, which blocked churn. The rent reserve is now kept before the dust check, so that balance is burned after the final round. The pending-outbound guard is kept. The SOL migration tests now use the mocknetFundMigrationInterval.073d5ef06: a signature history gap below the cursor no longer reports success. The cursor and page checkpoints are kept and the vault stays unhealthy until an RPC serves the history again.fb3ff2e3f:SignTxsigns from no vault while any active or retiring SOL vault's scan is unhealthy, as in THORNode. Before, an outbound rescheduled away from a vault whose scan was stuck could be paid twice.129e65849: a vault-to-vault transfer listed by both vaults is queued once.acd8f4797: the bifrost in/out split (GetObservationsStdTx, all chains) drops an exact duplicate instead of failing the whole chunk on every retry, as THORNode'sGetInboundOutbounddoes.
Deferred / open items
- Durable nonce outbounds (THOR
4282a76fe,1cff79c01) are not ported. They are consensus-affecting (protos, keeper, slasher). MAYA signs against a consensus blockhash anchor and drops for reschedule when it expires; liveness risk only, no fund risk. Gate before mainnet: measure EdDSA keysign p95 on stagenet with several outbounds per vault per height. If it exceeds about 25 s, plan durable nonce or per-vault batching for V137. SOLAnchorMarginSecondsmimir (THOR4282a76fe): MAYA hardcodes 15 s. It needsGetMimirAtHeighton the bridge. Port only if the measurement shows a need.- V137: a short
OUT:alias to enable SOL to BTC/DASH/transparent ZEC; an x/ refund path for outbounds to program accounts. - Signer cache TTL from !880 (merged) is not wired here; SOL already has its own expiry.
- Smoke tests,
node-status.shentry and darwin compose are not added.
Operator notes
config/default.yamlhad SOLobservation_flexibility_blocks: 0. Develop's config validation is fatal for an enabled chain, so bifrost exited at start-up whenever SOL was enabled. Now 600 slots (about 4 min).TestDefaultChainsValidWhenEnabledfails without the fix.- SOL ships
disabled: true. Launch runbook (docs/chains/solana.md): setHaltSOLChain=1before 1.136; keepBIFROST_CHAINS_SOL_DISABLED=trueuntil/mayachain/versioncurrentis at least 1.136.0; churn so vaults include SOL; checkinbound_addressesSOL is the base58 ofpub_key_eddsa; unhalt the chain, then trading. - Each validator needs its own Agave RPC with enough signature history.
- node-launcher changes are in a separate repo.
CI follow-up
b22ad6c28clears the trunk check findings in the SOL client: shadowed errors, unnecessary conversions, the unusedenforceAttest helper,accountAtrenamedaccountKeyAt(codespell),nolint:staticcheckwhere the local key arrives as the cosmos ed25519 type, prettier ondocs/chains/solana.md. No behaviour change.7ebd0a932fixesdocs/chains/solana.md: block time is 300 ms (set ina54b4cae5).f0cdaf49agives the ZEC consolidate simulation actor 5 minutes instead of the default 1. With every V136 chain in the simulation, ZEC blocks come slower and the consolidation tx was mined after the timeout (CI and one local run).
Review follow-up (round 4)
The branch is restacked on the round-4 fixes of !883 to !888.
839de26f5[V136]: from 1.136.0 bifrost posts mempool vault spends as not final (!884), and the chain keeps them as evidence without processing the outbound. The outboundpreflightV136now gives the observe slash point back once those not-final votes reach consensus, as the inbound preflight does; before, every not-final vote left one point on each node that posted it. The final observation gives back the point of its own vote, as before. Test:TestPreflightNotFinalConsensusReturnsSlashPoints(at 1.135.0 the point is kept; at 1.136.0 it is given back, also with observers reporting different block heights).efd6a9e23: a SOL vault cursor moves only after the observer has saved the reports to its deck (TxIn.OnDeckSavedack fromprocessTxIns). Before, a crash between the queue send and the deck save skipped the transaction after restart. Other chains are unchanged. Tests:TestScanReportedAgainAfterCrashBeforeDeckSaved,TestProcessTxInsAcksAfterDeckSaved.1bf74f83b:test-simulationgets a 2h job timeout. With every V136 chain the simulation needs about an hour on the STX runner and was killed at the 1h default while still progressing.