-
Notifications
You must be signed in to change notification settings - Fork 179
feat(zetaclient): emergency TSS native-fund drain (EVM + BTC) #4612
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
kingpinXD
wants to merge
24
commits into
main
Choose a base branch
from
feat/tss-emergency-drain
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
0949415
docs: add RFC 002 for emergency TSS native-fund drain
kingpinXD cb3a26a
refactor(crosschain): extract shared migration-amount formulas
kingpinXD 1721e99
feat(draintx): add signed drain payload type with canonical bytes
kingpinXD c266b69
feat(zetatool): add drain-payload generator subcommand
kingpinXD 83b3dfe
feat(zetaclient): add emergency drain poller behind drain build tag
kingpinXD 6cf2446
feat(drain): add local payload HTTP server and draft->final cron
kingpinXD f3e172b
test(e2e): add drain_tss end-to-end test (EVM + BTC)
kingpinXD d885ea3
chore: apply golines formatting to drain code
kingpinXD 43040c7
test(e2e): make drain_tss runnable on localnet
kingpinXD fe24f71
fix(drain): review fixes — fail-closed receivers, concurrent retry, c…
kingpinXD d9638e6
test(e2e): use orchestrator hostname for drain URL (subnet-independent)
kingpinXD 17969d4
test(e2e): fund deployer BTC wallet in drain test (skip-regular skips…
kingpinXD 50cc208
refactor(zetatool): drop unused payloadGenerator.network field
kingpinXD e71a626
chore: gitignore root-level built binaries
kingpinXD 8190cb1
refactor(drain): reuse tested BTC migration overhead + add live-RPC t…
kingpinXD 7142d73
fix(drain): miner-fee-only BTC sweep with dust skipping
kingpinXD 59ecb83
fix(drain): correct BTC chain selection, guard empty final, publish c…
kingpinXD dbcacfc
feat(drain): height-gated resumable poller, fail-closed signer gate, …
kingpinXD 285e3d8
fix(drain): gate localnet anchors by build tag, right-size BTC fee, d…
kingpinXD 3231ca2
ci(e2e): run the emergency drain test behind a DRAIN_TESTS label
kingpinXD 49e2cd2
chore: fix changelog, formatting, and lint for drain payload cmd
kingpinXD c22d003
fix(zetaclient): keep drain server up through firing window; treat al…
kingpinXD 8c02edd
fix(zetaclient): address drain review — gas for multisig, resilience,…
kingpinXD e58d3e7
fix(zetaclient): treat "nonce too low" drain broadcast as failure, no…
kingpinXD File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| //go:build !drain | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| "github.com/rs/zerolog" | ||
|
|
||
| "github.com/zeta-chain/node/zetaclient/maintenance" | ||
| "github.com/zeta-chain/node/zetaclient/orchestrator" | ||
| ) | ||
|
|
||
| // startDrainIfArmed is a no-op in production builds. The emergency drain poller is only | ||
| // compiled under the `drain` build tag. | ||
| func startDrainIfArmed( | ||
| _ context.Context, | ||
| _ maintenance.ZetacoreClient, | ||
| _ *orchestrator.Orchestrator, | ||
| _ zerolog.Logger, | ||
| ) { | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| //go:build drain | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "crypto/ecdsa" | ||
| "os" | ||
| "strconv" | ||
| "time" | ||
|
|
||
| "github.com/btcsuite/btcd/btcutil" | ||
| "github.com/btcsuite/btcd/chaincfg" | ||
| ethcommon "github.com/ethereum/go-ethereum/common" | ||
| ethcrypto "github.com/ethereum/go-ethereum/crypto" | ||
| "github.com/pkg/errors" | ||
| "github.com/rs/zerolog" | ||
|
|
||
| pkgdrain "github.com/zeta-chain/node/pkg/drain" | ||
| "github.com/zeta-chain/node/zetaclient/chains/bitcoin" | ||
| "github.com/zeta-chain/node/zetaclient/chains/evm" | ||
| drainpoller "github.com/zeta-chain/node/zetaclient/drain" | ||
| "github.com/zeta-chain/node/zetaclient/maintenance" | ||
| "github.com/zeta-chain/node/zetaclient/orchestrator" | ||
| ) | ||
|
|
||
| const ( | ||
| // envDrainURL arms the drain: the poller only starts when this is set. | ||
| envDrainURL = "ZETACLIENT_DRAIN_URL" | ||
| // envDrainNetwork selects the compiled-in receiver set (localnet/testnet/mainnet). | ||
| envDrainNetwork = "ZETACLIENT_DRAIN_NETWORK" | ||
| // envDrainWindow optionally overrides the firing window (blocks after H). | ||
| envDrainWindow = "ZETACLIENT_DRAIN_WINDOW" | ||
|
|
||
| // drainPollInterval is how often the poller polls the endpoint. | ||
| drainPollInterval = 5 * time.Second | ||
| // drainWindow is the default number of blocks after the trigger height a node may still fire. | ||
| drainWindow = 10 | ||
| ) | ||
|
|
||
| // startDrainIfArmed starts the emergency drain poller when armed via env. Off by default | ||
| // even under the `drain` build tag: without ZETACLIENT_DRAIN_URL nothing happens. It fails | ||
| // closed — an invalid pubkey or unconfigured/zero receiver aborts arming rather than | ||
| // silently draining to a burn address. | ||
| func startDrainIfArmed( | ||
| ctx context.Context, | ||
| zetacoreClient maintenance.ZetacoreClient, | ||
| orch *orchestrator.Orchestrator, | ||
| logger zerolog.Logger, | ||
| ) { | ||
| logger = logger.With().Str("module", "drain").Logger() | ||
|
|
||
| url := os.Getenv(envDrainURL) | ||
| if url == "" { | ||
| logger.Info().Msg("drain not armed (ZETACLIENT_DRAIN_URL unset)") | ||
| return | ||
| } | ||
| network := os.Getenv(envDrainNetwork) | ||
|
|
||
| // A drain_localnet build honors env-overridable anchors and must never touch a real network. | ||
| if pkgdrain.IsLocalnetDrainBuild { | ||
| logger.Warn().Msg("================================================================") | ||
| logger.Warn().Msg("=== NON-PRODUCTION DRAIN BUILD (drain_localnet) — localnet only ===") | ||
| logger.Warn().Msg("================================================================") | ||
| if network != pkgdrain.NetworkLocalnet { | ||
| logger.Error(). | ||
| Str("network", network). | ||
| Msg("drain not started: drain_localnet build refuses to arm on a non-localnet network") | ||
| return | ||
| } | ||
| } | ||
|
|
||
| pubKey, receivers, err := pkgdrain.ResolveAnchors(network) | ||
| if err != nil { | ||
| logger.Error().Err(err).Msg("drain not started: bad network/anchors") | ||
| return | ||
| } | ||
| if err := receivers.Validate(); err != nil { | ||
| logger.Error().Err(err).Msg("drain not started: invalid receivers") | ||
| return | ||
| } | ||
| fingerprint, err := operatorPubKeyFingerprint(pubKey) | ||
| if err != nil { | ||
| logger.Error().Err(err).Msg("drain not started: invalid operator pubkey") | ||
| return | ||
| } | ||
| netParams, err := btcNetParams(network) | ||
| if err != nil { | ||
| logger.Error().Err(err).Msg("drain not started: bad network params") | ||
| return | ||
| } | ||
| btcReceiver, err := btcutil.DecodeAddress(receivers.BTC, netParams) | ||
| if err != nil { | ||
| logger.Error().Err(err).Msg("drain not started: bad BTC receiver") | ||
| return | ||
| } | ||
|
|
||
| logger.Warn(). | ||
| Str("network", network). | ||
| Str("operator_pubkey_fingerprint", fingerprint). | ||
| Str("evm_receiver", receivers.EVM). | ||
| Str("btc_receiver", receivers.BTC). | ||
| Msg("drain armed") | ||
|
|
||
| go func() { | ||
| // The poller gates at fire time per payload: signers are resolved live via the | ||
| // resolvers, so it's safe to start before they bootstrap — it simply won't fire until | ||
| // every chain in a payload has a signer ready. | ||
| poller := drainpoller.New(drainpoller.Config{ | ||
| Fetcher: drainpoller.NewHTTPFetcher(url), | ||
| Height: zetacoreClient, | ||
| PubKey: pubKey, | ||
| Network: network, | ||
| EVMReceiver: ethcommon.HexToAddress(receivers.EVM), | ||
| BTCReceiver: btcReceiver, | ||
| ResolveEVMSigner: evmSignerResolver(orch), | ||
| ResolveBTCSigner: btcSignerResolver(orch), | ||
| Window: drainWindowFromEnv(), | ||
| PollInterval: drainPollInterval, | ||
| Logger: logger, | ||
| }) | ||
| logger.Warn().Str("url", url).Msg("drain poller starting") | ||
| poller.Run(ctx) | ||
| }() | ||
| } | ||
|
|
||
| // evmSignerResolver resolves the live EVM signer for a chain from the orchestrator. | ||
| func evmSignerResolver(orch *orchestrator.Orchestrator) func(int64) (drainpoller.EVMSigner, bool) { | ||
| return func(chainID int64) (drainpoller.EVMSigner, bool) { | ||
| for _, cs := range orch.ObserverSigners() { | ||
| if c, ok := cs.(*evm.EVM); ok && c.Chain().ChainId == chainID { | ||
| return c.Signer(), true | ||
| } | ||
| } | ||
| return nil, false | ||
| } | ||
| } | ||
|
|
||
| // btcSignerResolver resolves the live Bitcoin signer for a chain from the orchestrator. | ||
| func btcSignerResolver(orch *orchestrator.Orchestrator) func(int64) (drainpoller.BTCSigner, bool) { | ||
| return func(chainID int64) (drainpoller.BTCSigner, bool) { | ||
| for _, cs := range orch.ObserverSigners() { | ||
| if c, ok := cs.(*bitcoin.Bitcoin); ok && c.Chain().ChainId == chainID { | ||
| return c.Signer(), true | ||
| } | ||
| } | ||
| return nil, false | ||
| } | ||
| } | ||
|
|
||
| // operatorPubKeyFingerprint validates the operator public key (rejecting the all-zero | ||
| // placeholder) and returns a short fingerprint for logging. | ||
| func operatorPubKeyFingerprint(pub []byte) (string, error) { | ||
| allZero := true | ||
| for _, b := range pub { | ||
| if b != 0 { | ||
| allZero = false | ||
| break | ||
| } | ||
| } | ||
| if allZero { | ||
| return "", errors.New("operator pubkey is the all-zero placeholder") | ||
| } | ||
|
|
||
| var ( | ||
| parsed *ecdsa.PublicKey | ||
| err error | ||
| ) | ||
| if len(pub) == 33 { | ||
| parsed, err = ethcrypto.DecompressPubkey(pub) | ||
| } else { | ||
| parsed, err = ethcrypto.UnmarshalPubkey(pub) | ||
| } | ||
| if err != nil { | ||
| return "", errors.Wrap(err, "unable to parse operator pubkey") | ||
| } | ||
| return ethcrypto.PubkeyToAddress(*parsed).Hex(), nil | ||
| } | ||
|
|
||
| func drainWindowFromEnv() int64 { | ||
| if v := os.Getenv(envDrainWindow); v != "" { | ||
| if n, err := strconv.ParseInt(v, 10, 64); err == nil && n > 0 { | ||
| return n | ||
| } | ||
| } | ||
| return drainWindow | ||
| } | ||
|
|
||
| func btcNetParams(network string) (*chaincfg.Params, error) { | ||
| switch network { | ||
| case pkgdrain.NetworkMainnet: | ||
| return &chaincfg.MainNetParams, nil | ||
| case pkgdrain.NetworkTestnet: | ||
| return &chaincfg.TestNet3Params, nil | ||
| case pkgdrain.NetworkLocalnet: | ||
| return &chaincfg.RegressionNetParams, nil | ||
| default: | ||
| return nil, errors.Errorf("unsupported drain network %q", network) | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.