diff --git a/zetaclient/orchestrator/orchestrator.go b/zetaclient/orchestrator/orchestrator.go index 5f49469c49..4c8cb41734 100644 --- a/zetaclient/orchestrator/orchestrator.go +++ b/zetaclient/orchestrator/orchestrator.go @@ -24,6 +24,8 @@ import ( btcobserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/node/zetaclient/chains/interfaces" solanaobserver "github.com/zeta-chain/node/zetaclient/chains/solana/observer" + tonobserver "github.com/zeta-chain/node/zetaclient/chains/ton/observer" + tonsigner "github.com/zeta-chain/node/zetaclient/chains/ton/signer" zctx "github.com/zeta-chain/node/zetaclient/context" "github.com/zeta-chain/node/zetaclient/logs" "github.com/zeta-chain/node/zetaclient/metrics" @@ -193,6 +195,16 @@ func (oc *Orchestrator) resolveSigner(app *zctx.AppContext, chainID int64) (inte Str("signer.gateway_address", params.GatewayAddress). Msgf("updated gateway address for chain %d", chainID) } + case chain.IsTON(): + newAddress := chain.Params().GatewayAddress + + if newAddress != signer.GetGatewayAddress() { + signer.SetGatewayAddress(newAddress) + oc.logger.Info(). + Str("signer.new_gateway_address", newAddress). + Int64("signer.chain_id", chainID). + Msgf("set gateway address") + } } return signer, nil @@ -235,8 +247,9 @@ func (oc *Orchestrator) resolveObserver(app *zctx.AppContext, chainID int64) (in if !observertypes.ChainParamsEqual(curParams, *freshParams) { observer.SetChainParams(*freshParams) oc.logger.Info(). + Int64("observer.chain_id", chainID). Interface("observer.chain_params", *freshParams). - Msgf("updated chain params for chainID %d", chainID) + Msg("updated chain params") } return observer, nil @@ -416,6 +429,8 @@ func (oc *Orchestrator) runScheduler(ctx context.Context) error { oc.ScheduleCctxBTC(ctx, zetaHeight, chainID, cctxList, ob, signer) case chain.IsSolana(): oc.ScheduleCctxSolana(ctx, zetaHeight, chainID, cctxList, ob, signer) + case chain.IsTON(): + oc.ScheduleCCTXTON(ctx, zetaHeight, chainID, cctxList, ob, signer) default: oc.logger.Error().Msgf("runScheduler: no scheduler found chain %d", chainID) continue @@ -665,6 +680,87 @@ func (oc *Orchestrator) ScheduleCctxSolana( } } +// ScheduleCCTXTON schedules TON outbound keySign on each ZetaChain block +func (oc *Orchestrator) ScheduleCCTXTON( + ctx context.Context, + zetaHeight uint64, + chainID int64, + cctxList []*types.CrossChainTx, + observer interfaces.ChainObserver, + signer interfaces.ChainSigner, +) { + // should never happen + if _, ok := observer.(*tonobserver.Observer); !ok { + oc.logger.Error().Msgf("ScheduleCCTXTON: observer is not TON") + return + } + + if _, ok := signer.(*tonsigner.Signer); !ok { + oc.logger.Error().Msgf("ScheduleCCTXTON: signer is not TON") + return + } + + // Scheduler interval measured in zeta blocks. + // runScheduler() guarantees that this function is called every zeta block. + // Note that TON blockchain is async and doesn't have a concept of confirmations + // i.e. tx is finalized as soon as it's included in the next block (less than 6 seconds) + // #nosec G701 positive + interval := uint64(observer.ChainParams().OutboundScheduleInterval) + + shouldProcessOutbounds := zetaHeight%interval == 0 + + for i := range cctxList { + var ( + cctx = cctxList[i] + params = cctx.GetCurrentOutboundParam() + nonce = params.TssNonce + outboundID = outboundprocessor.ToOutboundID(cctx.Index, params.ReceiverChainId, nonce) + ) + + if params.ReceiverChainId != chainID { + // should not happen + oc.logger.Error().Msgf("ScheduleCCTXTON: outbound chain id mismatch (got %d)", params.ReceiverChainId) + continue + } + + // vote outbound if it's already confirmed + continueKeySign, err := observer.VoteOutboundIfConfirmed(ctx, cctx) + + switch { + case err != nil: + oc.logger.Error().Err(err).Uint64("outbound.nonce", nonce). + Msg("ScheduleCCTXTON: VoteOutboundIfConfirmed failed") + continue + case !continueKeySign: + oc.logger.Info().Uint64("outbound.nonce", nonce). + Msg("ScheduleCCTXTON: outbound already processed") + continue + case !shouldProcessOutbounds: + // well, let's wait for another block to (probably) trigger the processing + continue + } + + // try to sign and broadcast cctx to TON + task := func(ctx context.Context) error { + signer.TryProcessOutbound( + ctx, + cctx, + oc.outboundProc, + outboundID, + observer, + oc.zetacoreClient, + zetaHeight, + ) + + return nil + } + + // fire async task + taskLogger := oc.logger.Logger.With().Str("outbound.id", outboundID).Logger() + bg.Work(ctx, task, bg.WithName("TryProcessOutbound"), bg.WithLogger(taskLogger)) + } +} + // runObserverSignerSync runs a blocking ticker that observes chain changes from zetacore // and optionally (de)provisions respective observers and signers. func (oc *Orchestrator) runObserverSignerSync(ctx context.Context) error { diff --git a/zetaclient/orchestrator/orchestrator_test.go b/zetaclient/orchestrator/orchestrator_test.go index 2ab34b900e..d88006920f 100644 --- a/zetaclient/orchestrator/orchestrator_test.go +++ b/zetaclient/orchestrator/orchestrator_test.go @@ -31,12 +31,14 @@ func Test_GetUpdatedSigner(t *testing.T) { evmChain = chains.Ethereum btcChain = chains.BitcoinMainnet solChain = chains.SolanaMainnet + tonChain = chains.TONMainnet ) var ( evmChainParams = mocks.MockChainParams(evmChain.ChainId, 100) btcChainParams = mocks.MockChainParams(btcChain.ChainId, 100) solChainParams = mocks.MockChainParams(solChain.ChainId, 100) + tonChainParams = mocks.MockChainParams(tonChain.ChainId, 100) ) solChainParams.GatewayAddress = solanacontracts.SolanaGatewayProgramID @@ -50,6 +52,9 @@ func Test_GetUpdatedSigner(t *testing.T) { solChainParamsNew := mocks.MockChainParams(solChain.ChainId, 100) solChainParamsNew.GatewayAddress = sample.SolanaAddress(t) + tonChainParamsNew := mocks.MockChainParams(tonChain.ChainId, 100) + tonChainParamsNew.GatewayAddress = sample.GenerateTONAccountID().ToRaw() + t.Run("signer should not be found", func(t *testing.T) { orchestrator := mockOrchestrator(t, nil, evmChain, btcChain, evmChainParams, btcChainParams) appContext := createAppContext(t, evmChain, btcChain, evmChainParamsNew, btcChainParams) @@ -86,6 +91,23 @@ func Test_GetUpdatedSigner(t *testing.T) { require.NoError(t, err) require.Equal(t, solChainParamsNew.GatewayAddress, signer.GetGatewayAddress()) }) + + t.Run("should be able to update ton gateway address", func(t *testing.T) { + orchestrator := mockOrchestrator(t, nil, + evmChain, btcChain, solChain, tonChain, + evmChainParams, btcChainParams, solChainParams, tonChainParams, + ) + + appContext := createAppContext(t, + evmChain, btcChain, solChain, tonChain, + evmChainParams, btcChainParams, solChainParamsNew, tonChainParamsNew, + ) + + // update signer with new gateway address + signer, err := orchestrator.resolveSigner(appContext, tonChain.ChainId) + require.NoError(t, err) + require.Equal(t, tonChainParamsNew.GatewayAddress, signer.GetGatewayAddress()) + }) } func Test_GetUpdatedChainObserver(t *testing.T) { @@ -94,15 +116,18 @@ func Test_GetUpdatedChainObserver(t *testing.T) { evmChain = chains.Ethereum btcChain = chains.BitcoinMainnet solChain = chains.SolanaMainnet + tonChain = chains.TONMainnet ) var ( evmChainParams = mocks.MockChainParams(evmChain.ChainId, 100) btcChainParams = mocks.MockChainParams(btcChain.ChainId, 100) solChainParams = mocks.MockChainParams(solChain.ChainId, 100) + tonChainParams = mocks.MockChainParams(tonChain.ChainId, 100) ) solChainParams.GatewayAddress = solanacontracts.SolanaGatewayProgramID + tonChainParams.GatewayAddress = sample.GenerateTONAccountID().ToRaw() // new chain params in AppContext evmChainParamsNew := &observertypes.ChainParams{ @@ -153,6 +178,22 @@ func Test_GetUpdatedChainObserver(t *testing.T) { MinObserverDelegation: sdk.OneDec(), IsSupported: true, } + tonChainParamsNew := &observertypes.ChainParams{ + ChainId: tonChain.ChainId, + ConfirmationCount: 10, + GasPriceTicker: 5, + InboundTicker: 6, + OutboundTicker: 6, + WatchUtxoTicker: 1, + ZetaTokenContractAddress: "", + ConnectorContractAddress: "", + Erc20CustodyContractAddress: "", + OutboundScheduleInterval: 10, + OutboundScheduleLookahead: 10, + BallotThreshold: sdk.OneDec(), + MinObserverDelegation: sdk.OneDec(), + IsSupported: true, + } t.Run("evm chain observer should not be found", func(t *testing.T) { orchestrator := mockOrchestrator( @@ -284,6 +325,43 @@ func Test_GetUpdatedChainObserver(t *testing.T) { require.NotNil(t, chainOb) require.True(t, observertypes.ChainParamsEqual(*solChainParamsNew, chainOb.ChainParams())) }) + t.Run("ton chain observer should not be found", func(t *testing.T) { + orchestrator := mockOrchestrator( + t, + nil, + evmChain, btcChain, solChain, + evmChainParams, btcChainParams, solChainParams, + ) + + appContext := createAppContext( + t, + evmChain, + btcChain, + solChain, + evmChainParams, + btcChainParams, + solChainParamsNew, + ) + + _, err := orchestrator.resolveObserver(appContext, tonChain.ChainId) + require.ErrorContains(t, err, "observer not found") + }) + t.Run("chain params in ton chain observer should be updated successfully", func(t *testing.T) { + orchestrator := mockOrchestrator(t, nil, + evmChain, btcChain, tonChain, + evmChainParams, btcChainParams, tonChainParams, + ) + appContext := createAppContext(t, + evmChain, btcChain, tonChain, + evmChainParams, btcChainParams, tonChainParamsNew, + ) + + // update solana chain observer with new chain params + chainOb, err := orchestrator.resolveObserver(appContext, tonChain.ChainId) + require.NoError(t, err) + require.NotNil(t, chainOb) + require.True(t, observertypes.ChainParamsEqual(*tonChainParamsNew, chainOb.ChainParams())) + }) } func Test_GetPendingCctxsWithinRateLimit(t *testing.T) { @@ -500,6 +578,9 @@ func mockOrchestrator(t *testing.T, zetaClient interfaces.ZetacoreClient, chains case chains.IsSolanaChain(cp.ChainId, nil): observers[cp.ChainId] = mocks.NewSolanaObserver(cp) signers[cp.ChainId] = mocks.NewSolanaSigner() + case chains.IsTONChain(cp.ChainId, nil): + observers[cp.ChainId] = mocks.NewTONObserver(cp) + signers[cp.ChainId] = mocks.NewTONSigner() default: t.Fatalf("mock orcestrator: unsupported chain %d", cp.ChainId) } @@ -526,6 +607,8 @@ func createAppContext(t *testing.T, chainsOrParams ...any) *zctx.AppContext { cfg.BTCChainConfigs[c.ChainId] = config.BTCConfig{RPCHost: "localhost"} case chains.IsSolanaChain(c.ChainId, nil): cfg.SolanaConfig = config.SolanaConfig{Endpoint: "localhost"} + case chains.IsTONChain(c.ChainId, nil): + cfg.TONConfig = config.TONConfig{LiteClientConfigURL: "localhost"} default: t.Fatalf("create app context: unsupported chain %d", c.ChainId) } diff --git a/zetaclient/testutils/mocks/chain_clients.go b/zetaclient/testutils/mocks/chain_clients.go index aa5e36889b..9ec45cbd90 100644 --- a/zetaclient/testutils/mocks/chain_clients.go +++ b/zetaclient/testutils/mocks/chain_clients.go @@ -3,11 +3,22 @@ package mocks import ( "context" - crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + cc "github.com/zeta-chain/node/x/crosschain/types" observertypes "github.com/zeta-chain/node/x/observer/types" "github.com/zeta-chain/node/zetaclient/chains/interfaces" ) +type DummyObserver struct{} + +func (ob *DummyObserver) VoteOutboundIfConfirmed(_ context.Context, _ *cc.CrossChainTx) (bool, error) { + return false, nil +} + +func (ob *DummyObserver) Start(_ context.Context) {} +func (ob *DummyObserver) Stop() {} +func (ob *DummyObserver) SetChainParams(_ observertypes.ChainParams) {} +func (ob *DummyObserver) ChainParams() (_ observertypes.ChainParams) { return } + // ---------------------------------------------------------------------------- // EVMObserver // ---------------------------------------------------------------------------- @@ -15,23 +26,12 @@ var _ interfaces.ChainObserver = (*EVMObserver)(nil) // EVMObserver is a mock of evm chain observer for testing type EVMObserver struct { + DummyObserver chainParams observertypes.ChainParams } func NewEVMObserver(chainParams *observertypes.ChainParams) *EVMObserver { - return &EVMObserver{ - chainParams: *chainParams, - } -} - -func (ob *EVMObserver) Start(_ context.Context) {} -func (ob *EVMObserver) Stop() {} - -func (ob *EVMObserver) VoteOutboundIfConfirmed( - _ context.Context, - _ *crosschaintypes.CrossChainTx, -) (bool, error) { - return false, nil + return &EVMObserver{chainParams: *chainParams} } func (ob *EVMObserver) SetChainParams(chainParams observertypes.ChainParams) { @@ -42,14 +42,6 @@ func (ob *EVMObserver) ChainParams() observertypes.ChainParams { return ob.chainParams } -func (ob *EVMObserver) GetTxID(_ uint64) string { - return "" -} - -func (ob *EVMObserver) WatchInboundTracker(_ context.Context) error { - return nil -} - // ---------------------------------------------------------------------------- // BTCObserver // ---------------------------------------------------------------------------- @@ -57,24 +49,12 @@ var _ interfaces.ChainObserver = (*BTCObserver)(nil) // BTCObserver is a mock of btc chain observer for testing type BTCObserver struct { + DummyObserver chainParams observertypes.ChainParams } func NewBTCObserver(chainParams *observertypes.ChainParams) *BTCObserver { - return &BTCObserver{ - chainParams: *chainParams, - } -} - -func (ob *BTCObserver) Start(_ context.Context) {} - -func (ob *BTCObserver) Stop() {} - -func (ob *BTCObserver) VoteOutboundIfConfirmed( - _ context.Context, - _ *crosschaintypes.CrossChainTx, -) (bool, error) { - return false, nil + return &BTCObserver{chainParams: *chainParams} } func (ob *BTCObserver) SetChainParams(chainParams observertypes.ChainParams) { @@ -85,12 +65,6 @@ func (ob *BTCObserver) ChainParams() observertypes.ChainParams { return ob.chainParams } -func (ob *BTCObserver) GetTxID(_ uint64) string { - return "" -} - -func (ob *BTCObserver) WatchInboundTracker(_ context.Context) error { return nil } - // ---------------------------------------------------------------------------- // SolanaObserver // ---------------------------------------------------------------------------- @@ -98,24 +72,12 @@ var _ interfaces.ChainObserver = (*SolanaObserver)(nil) // SolanaObserver is a mock of solana chain observer for testing type SolanaObserver struct { + DummyObserver chainParams observertypes.ChainParams } func NewSolanaObserver(chainParams *observertypes.ChainParams) *SolanaObserver { - return &SolanaObserver{ - chainParams: *chainParams, - } -} - -func (ob *SolanaObserver) Start(_ context.Context) {} - -func (ob *SolanaObserver) Stop() {} - -func (ob *SolanaObserver) VoteOutboundIfConfirmed( - _ context.Context, - _ *crosschaintypes.CrossChainTx, -) (bool, error) { - return false, nil + return &SolanaObserver{chainParams: *chainParams} } func (ob *SolanaObserver) SetChainParams(chainParams observertypes.ChainParams) { @@ -126,8 +88,25 @@ func (ob *SolanaObserver) ChainParams() observertypes.ChainParams { return ob.chainParams } -func (ob *SolanaObserver) GetTxID(_ uint64) string { - return "" +// ---------------------------------------------------------------------------- +// SolanaObserver +// ---------------------------------------------------------------------------- +var _ interfaces.ChainObserver = (*TONObserver)(nil) + +// TONObserver is a mock of TON chain observer for testing +type TONObserver struct { + DummyObserver + chainParams observertypes.ChainParams } -func (ob *SolanaObserver) WatchInboundTracker(_ context.Context) error { return nil } +func NewTONObserver(chainParams *observertypes.ChainParams) *TONObserver { + return &TONObserver{chainParams: *chainParams} +} + +func (ob *TONObserver) SetChainParams(chainParams observertypes.ChainParams) { + ob.chainParams = chainParams +} + +func (ob *TONObserver) ChainParams() observertypes.ChainParams { + return ob.chainParams +} diff --git a/zetaclient/testutils/mocks/chain_signer.go b/zetaclient/testutils/mocks/chain_signer.go index cc0c65457f..790ee8fd92 100644 --- a/zetaclient/testutils/mocks/chain_signer.go +++ b/zetaclient/testutils/mocks/chain_signer.go @@ -11,6 +11,26 @@ import ( "github.com/zeta-chain/node/zetaclient/outboundprocessor" ) +type DummySigner struct{} + +func (s *DummySigner) TryProcessOutbound( + _ context.Context, + _ *crosschaintypes.CrossChainTx, + _ *outboundprocessor.Processor, + _ string, + _ interfaces.ChainObserver, + _ interfaces.ZetacoreClient, + _ uint64, +) { +} + +func (s *DummySigner) SetGatewayAddress(_ string) {} +func (s *DummySigner) GetGatewayAddress() (_ string) { return } +func (s *DummySigner) SetZetaConnectorAddress(_ ethcommon.Address) {} +func (s *DummySigner) SetERC20CustodyAddress(_ ethcommon.Address) {} +func (s *DummySigner) GetZetaConnectorAddress() (_ ethcommon.Address) { return } +func (s *DummySigner) GetERC20CustodyAddress() (_ ethcommon.Address) { return } + // ---------------------------------------------------------------------------- // EVMSigner // ---------------------------------------------------------------------------- @@ -18,6 +38,7 @@ var _ interfaces.ChainSigner = (*EVMSigner)(nil) // EVMSigner is a mock of evm chain signer for testing type EVMSigner struct { + DummySigner Chain chains.Chain ZetaConnectorAddress ethcommon.Address ERC20CustodyAddress ethcommon.Address @@ -35,24 +56,6 @@ func NewEVMSigner( } } -func (s *EVMSigner) TryProcessOutbound( - _ context.Context, - _ *crosschaintypes.CrossChainTx, - _ *outboundprocessor.Processor, - _ string, - _ interfaces.ChainObserver, - _ interfaces.ZetacoreClient, - _ uint64, -) { -} - -func (s *EVMSigner) SetGatewayAddress(_ string) { -} - -func (s *EVMSigner) GetGatewayAddress() string { - return "" -} - func (s *EVMSigner) SetZetaConnectorAddress(address ethcommon.Address) { s.ZetaConnectorAddress = address } @@ -75,45 +78,12 @@ func (s *EVMSigner) GetERC20CustodyAddress() ethcommon.Address { var _ interfaces.ChainSigner = (*BTCSigner)(nil) // BTCSigner is a mock of bitcoin chain signer for testing -type BTCSigner struct { -} +type BTCSigner = DummySigner func NewBTCSigner() *BTCSigner { return &BTCSigner{} } -func (s *BTCSigner) TryProcessOutbound( - _ context.Context, - _ *crosschaintypes.CrossChainTx, - _ *outboundprocessor.Processor, - _ string, - _ interfaces.ChainObserver, - _ interfaces.ZetacoreClient, - _ uint64, -) { -} - -func (s *BTCSigner) SetGatewayAddress(_ string) { -} - -func (s *BTCSigner) GetGatewayAddress() string { - return "" -} - -func (s *BTCSigner) SetZetaConnectorAddress(_ ethcommon.Address) { -} - -func (s *BTCSigner) SetERC20CustodyAddress(_ ethcommon.Address) { -} - -func (s *BTCSigner) GetZetaConnectorAddress() ethcommon.Address { - return ethcommon.Address{} -} - -func (s *BTCSigner) GetERC20CustodyAddress() ethcommon.Address { - return ethcommon.Address{} -} - // ---------------------------------------------------------------------------- // SolanaSigner // ---------------------------------------------------------------------------- @@ -121,6 +91,7 @@ var _ interfaces.ChainSigner = (*SolanaSigner)(nil) // SolanaSigner is a mock of solana chain signer for testing type SolanaSigner struct { + DummySigner GatewayAddress string } @@ -128,17 +99,6 @@ func NewSolanaSigner() *SolanaSigner { return &SolanaSigner{} } -func (s *SolanaSigner) TryProcessOutbound( - _ context.Context, - _ *crosschaintypes.CrossChainTx, - _ *outboundprocessor.Processor, - _ string, - _ interfaces.ChainObserver, - _ interfaces.ZetacoreClient, - _ uint64, -) { -} - func (s *SolanaSigner) SetGatewayAddress(address string) { s.GatewayAddress = address } @@ -147,16 +107,25 @@ func (s *SolanaSigner) GetGatewayAddress() string { return s.GatewayAddress } -func (s *SolanaSigner) SetZetaConnectorAddress(_ ethcommon.Address) { +// ---------------------------------------------------------------------------- +// TONSigner +// ---------------------------------------------------------------------------- +var _ interfaces.ChainSigner = (*TONSigner)(nil) + +// TONSigner is a mock of TON chain signer for testing +type TONSigner struct { + DummySigner + GatewayAddress string } -func (s *SolanaSigner) SetERC20CustodyAddress(_ ethcommon.Address) { +func NewTONSigner() *TONSigner { + return &TONSigner{} } -func (s *SolanaSigner) GetZetaConnectorAddress() ethcommon.Address { - return ethcommon.Address{} +func (s *TONSigner) SetGatewayAddress(address string) { + s.GatewayAddress = address } -func (s *SolanaSigner) GetERC20CustodyAddress() ethcommon.Address { - return ethcommon.Address{} +func (s *TONSigner) GetGatewayAddress() string { + return s.GatewayAddress }