[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% of maya1…/thor1… addresses also decode to 32 bytes of base58.
  • a54b4cae5: ApproximateBlockMilliseconds(SOL) = 300. Only queries and the SOL scanner use it.
  • 6a2677c29: FuzzNewAddressV136 over every V136 chain.

State machine ([V136])

  • 6f98d4b53: the memo parser reads OUT:/REFUND: tx ids with NewTxIDV136 from 1.136.0 and with NewTxID before. 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 use NewTxIDV136.
  • 91f67ef63: SOL in SUPPORT_CHAINS_VCURRENT (mainnet, stagenet, mocknet).
  • 014384470: archive TxOutStorageV133 (manager_txout_v133.go) and NetworkMgrV135 (manager_network_v135.go). Byte copies, type renamed. 1.136.0 returns 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/ObservedTxOut handleV136 compare SOL self-sends exactly. The txin one builds on handleV135, the txout one on handleV133. 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 fail ValidateBasic (code 6) before 1.136.0 through the activation gate, and pass from 1.136.0.

Bifrost ([Version-unspecific])

  • 158a7019a: plumbing. Bridge GetBlockTimestamp, LevelDB SOL cursor, exact SOL match in pubkey manager and observer, NewTxIDV136 in the observer, solvency, metrics, config (disabled: true).
  • 0d2c60682: bifrost/pkg/chainclients/solana client, 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 suites suites/sol/{swap,refund-sol-to-btc,version-gate}.yaml, sol_txid / sol_txid_86 templates.
  • 42bcb0c14: regenerated exports.

Versioning and consensus impact

  • New V136 code: NewTxIDV136 and its memo gate, SOL cases in the V136 address set, TxOut and network VCUR (from 1.136.0), observed-tx handleV136 and preflightV136, the exact-case voter functions (Tx.EqualsExV136, ObservedTxVoter.AddV136 / GetTxV136).
  • Frozen bodies moved unchanged: TxOutStorageV133, NetworkMgrV135, observed txin handleV135 / handleV133 / handleV127 / preflightV135, observed txout handleV133 / preflightV135.
  • lint-versions passes; #check-lint-warning is not needed.
  • 9b0ac9c59 and a54b4cae5 change V136 functions in place. V136 is unreleased.
  • Not changed: archived memo parsers, the frozen slasher REFUND: re-parse (falls back to the TxOutItem InHash), 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 test and make test-network-specific pass. Coverage includes the activation rule (every predicate equals Chain("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-ZEC timeout, 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 mocknet FundMigrationInterval.
  • 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: SignTx signs 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's GetInboundOutbound does.

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.
  • SOLAnchorMarginSeconds mimir (THOR 4282a76fe): MAYA hardcodes 15 s. It needs GetMimirAtHeight on 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.sh entry and darwin compose are not added.

Operator notes

  • config/default.yaml had SOL observation_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). TestDefaultChainsValidWhenEnabled fails without the fix.
  • SOL ships disabled: true. Launch runbook (docs/chains/solana.md): set HaltSOLChain=1 before 1.136; keep BIFROST_CHAINS_SOL_DISABLED=true until /mayachain/version current is at least 1.136.0; churn so vaults include SOL; check inbound_addresses SOL is the base58 of pub_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

  • b22ad6c28 clears the trunk check findings in the SOL client: shadowed errors, unnecessary conversions, the unused enforceAt test helper, accountAt renamed accountKeyAt (codespell), nolint:staticcheck where the local key arrives as the cosmos ed25519 type, prettier on docs/chains/solana.md. No behaviour change.
  • 7ebd0a932 fixes docs/chains/solana.md: block time is 300 ms (set in a54b4cae5).
  • f0cdaf49a gives 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 outbound preflightV136 now 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.OnDeckSaved ack from processTxIns). 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-simulation gets 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.
Edited by Bitol Maya

Merge request reports

Loading
Loading