diff --git a/cmd/start.go b/cmd/start.go index 367d18e9e..35958fb28 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -3,9 +3,10 @@ package cmd import ( "context" "fmt" - "github.com/keep-network/keep-core/pkg/diagnostics" "time" + "github.com/keep-network/keep-core/pkg/diagnostics" + "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/metrics" "github.com/keep-network/keep-core/pkg/net" @@ -19,11 +20,12 @@ import ( "github.com/keep-network/keep-core/pkg/net/libp2p" "github.com/keep-network/keep-core/pkg/net/retransmission" "github.com/keep-network/keep-core/pkg/operator" - "github.com/keep-network/keep-ecdsa/pkg/firewall" "github.com/keep-network/keep-ecdsa/internal/config" "github.com/keep-network/keep-ecdsa/pkg/chain/ethereum" "github.com/keep-network/keep-ecdsa/pkg/client" + "github.com/keep-network/keep-ecdsa/pkg/extensions/tbtc" + "github.com/keep-network/keep-ecdsa/pkg/firewall" "github.com/urfave/cli" ) @@ -168,6 +170,8 @@ func Start(c *cli.Context) error { ) logger.Debugf("initialized operator with address: [%s]", ethereumKey.Address.String()) + initializeExtensions(ctx, config.Extensions, ethereumChain) + initializeMetrics(ctx, config, networkProvider, stakeMonitor, ethereumKey.Address.Hex()) initializeDiagnostics(config, networkProvider) @@ -183,6 +187,35 @@ func Start(c *cli.Context) error { } } +func initializeExtensions( + ctx context.Context, + config config.Extensions, + ethereumChain *ethereum.EthereumChain, +) { + if len(config.TBTC.TBTCSystem) > 0 { + tbtcEthereumChain, err := ethereum.WithTBTCExtension( + ethereumChain, + config.TBTC.TBTCSystem, + ) + if err != nil { + logger.Errorf( + "could not initialize tbtc chain extension: [%v]", + err, + ) + return + } + + err = tbtc.Initialize(ctx, tbtcEthereumChain) + if err != nil { + logger.Errorf( + "could not initialize tbtc extension: [%v]", + err, + ) + return + } + } +} + func initializeMetrics( ctx context.Context, config *config.Config, diff --git a/configs/config.toml.SAMPLE b/configs/config.toml.SAMPLE index 2a3c505c1..abb7cb026 100644 --- a/configs/config.toml.SAMPLE +++ b/configs/config.toml.SAMPLE @@ -108,4 +108,12 @@ # The port on which the `/diagnostics` endpoint will be available can be # customized below. # [Diagnostics] - # Port = 8081 \ No newline at end of file + # Port = 8081 + +# Uncomment to enable tBTC-specific extension. This extension takes care of +# executing actions that are assumed by tBTC to be the signer's responsibility, +# for example, retrieve public key from keep to tBTC deposit or +# increase redemption fee on tBTC deposit. +# [Extensions.TBTC] + # TBTCSystem = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + diff --git a/go.mod b/go.mod index 1817fd3e6..46c3db432 100644 --- a/go.mod +++ b/go.mod @@ -18,8 +18,9 @@ require ( github.com/gogo/protobuf v1.3.1 github.com/google/gofuzz v1.1.0 github.com/ipfs/go-log v1.0.4 - github.com/keep-network/keep-common v1.2.1-0.20201002105641-e04cc579ff66 + github.com/keep-network/keep-common v1.2.1-0.20201020114759-19c123cbd4f4 github.com/keep-network/keep-core v1.3.0 + github.com/keep-network/tbtc v1.1.1-0.20201020115551-5f9077c74826 github.com/pkg/errors v0.9.1 github.com/urfave/cli v1.22.1 ) diff --git a/go.sum b/go.sum index adae734e8..e89cf4f9e 100644 --- a/go.sum +++ b/go.sum @@ -333,10 +333,12 @@ github.com/keep-network/go-libp2p-bootstrap v0.0.0-20200423153828-ed815bc50aec h github.com/keep-network/go-libp2p-bootstrap v0.0.0-20200423153828-ed815bc50aec/go.mod h1:xR8jf3/VJAjh3nWu5tFe8Yxnt2HvWsqZHfGef1P5oDk= github.com/keep-network/keep-common v1.2.0 h1:hVd2tTd7vL+9CQP5Ntk5kjs+GYvkgrRNBcNvTuhHhVk= github.com/keep-network/keep-common v1.2.0/go.mod h1:emxogTbBdey7M3jOzfxZOdfn139kN2mI2b2wA6AHKKo= -github.com/keep-network/keep-common v1.2.1-0.20201002105641-e04cc579ff66 h1:x/9fra2wLrBDhS8LOQztFajoM2q13FgBN5jFiqBGBog= -github.com/keep-network/keep-common v1.2.1-0.20201002105641-e04cc579ff66/go.mod h1:emxogTbBdey7M3jOzfxZOdfn139kN2mI2b2wA6AHKKo= +github.com/keep-network/keep-common v1.2.1-0.20201020114759-19c123cbd4f4 h1:CivupPSFswHACua5xZGKdeYxsCQ2cmRomTIBh8kfk70= +github.com/keep-network/keep-common v1.2.1-0.20201020114759-19c123cbd4f4/go.mod h1:emxogTbBdey7M3jOzfxZOdfn139kN2mI2b2wA6AHKKo= github.com/keep-network/keep-core v1.3.0 h1:7Tb33EmO/ntHOEbOiYciRlBhqu5Ln6KemWCaYK0Z6LA= github.com/keep-network/keep-core v1.3.0/go.mod h1:1KsSSTQoN754TrFLW7kLy50pOG2CQ4BOfnJqdvEG7FA= +github.com/keep-network/tbtc v1.1.1-0.20201020115551-5f9077c74826 h1:ijlpSs+mEtur4F1DQA8450Ubuhdk4lGjIoPZr3yf7vc= +github.com/keep-network/tbtc v1.1.1-0.20201020115551-5f9077c74826/go.mod h1:igBF2MPTFkzOdZ3gcwt8h0Zb5pZaHnij/iPZoMB9IKM= github.com/keep-network/toml v0.3.0 h1:G+NJwWR/ZiORqeLBsDXDchYoL29PXHdxOPcCueA7ctE= github.com/keep-network/toml v0.3.0/go.mod h1:Zeyd3lxbIlMYLREho3UK1dMP2xjqt2gLkQ5E5vM6K38= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= diff --git a/internal/config/config.go b/internal/config/config.go index ff224b1d2..4cbcf092d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -27,6 +27,7 @@ type Config struct { TSS tss.Config Metrics Metrics Diagnostics Diagnostics + Extensions Extensions } // SanctionedApplications contains addresses of applications approved by the @@ -70,6 +71,17 @@ type Diagnostics struct { Port int } +// Extensions stores app-specific extensions configuration. +type Extensions struct { + TBTC TBTC +} + +// TBTC stores configuration of application extension responsible for +// executing signer actions specific for TBTC application. +type TBTC struct { + TBTCSystem string +} + // ReadConfig reads in the configuration file in .toml format. Ethereum key file // password is expected to be provided as environment variable. func ReadConfig(filePath string) (*Config, error) { diff --git a/pkg/chain/ethereum/connect.go b/pkg/chain/ethereum/connect.go index 337eb6343..5a7e059f2 100644 --- a/pkg/chain/ethereum/connect.go +++ b/pkg/chain/ethereum/connect.go @@ -14,7 +14,6 @@ import ( "github.com/keep-network/keep-common/pkg/chain/ethereum" "github.com/keep-network/keep-common/pkg/chain/ethereum/blockcounter" "github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil" - eth "github.com/keep-network/keep-ecdsa/pkg/chain" "github.com/keep-network/keep-ecdsa/pkg/chain/gen/contract" ) @@ -60,7 +59,7 @@ type EthereumChain struct { // Connect performs initialization for communication with Ethereum blockchain // based on provided config. -func Connect(accountKey *keystore.Key, config *ethereum.Config) (eth.Handle, error) { +func Connect(accountKey *keystore.Key, config *ethereum.Config) (*EthereumChain, error) { client, err := ethclient.Dial(config.URL) if err != nil { return nil, err diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go new file mode 100644 index 000000000..a61798240 --- /dev/null +++ b/pkg/chain/ethereum/tbtc.go @@ -0,0 +1,153 @@ +package ethereum + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/keep-network/keep-common/pkg/subscription" + "github.com/keep-network/tbtc/pkg/chain/ethereum/gen/contract" +) + +// TBTCEthereumChain represents an Ethereum chain handle with +// TBTC-specific capabilities. +type TBTCEthereumChain struct { + *EthereumChain + + tbtcSystemContract *contract.TBTCSystem +} + +// WithTBTCExtension extends the Ethereum chain handle with +// TBTC-specific capabilities. +func WithTBTCExtension( + ethereumChain *EthereumChain, + tbtcSystemContractAddress string, +) (*TBTCEthereumChain, error) { + if !common.IsHexAddress(tbtcSystemContractAddress) { + return nil, fmt.Errorf("incorrect TBTCSystem contract address") + } + + tbtcSystemContract, err := contract.NewTBTCSystem( + common.HexToAddress(tbtcSystemContractAddress), + ethereumChain.accountKey, + ethereumChain.client, + ethereumChain.nonceManager, + ethereumChain.miningWaiter, + ethereumChain.transactionMutex, + ) + if err != nil { + return nil, err + } + + return &TBTCEthereumChain{ + EthereumChain: ethereumChain, + tbtcSystemContract: tbtcSystemContract, + }, nil +} + +// OnDepositCreated installs a callback that is invoked when an +// on-chain notification of a new deposit creation is seen. +func (tec *TBTCEthereumChain) OnDepositCreated( + handler func(depositAddress string), +) (subscription.EventSubscription, error) { + return tec.tbtcSystemContract.WatchCreated( + func( + DepositContractAddress common.Address, + KeepAddress common.Address, + Timestamp *big.Int, + blockNumber uint64, + ) { + handler(DepositContractAddress.Hex()) + }, + func(err error) error { + return fmt.Errorf("watch deposit created failed: [%v]", err) + }, + nil, + nil, + ) +} + +// OnDepositRegisteredPubkey installs a callback that is invoked when an +// on-chain notification of a deposit's pubkey registration is seen. +func (tec *TBTCEthereumChain) OnDepositRegisteredPubkey( + handler func(depositAddress string), +) (subscription.EventSubscription, error) { + return tec.tbtcSystemContract.WatchRegisteredPubkey( + func( + DepositContractAddress common.Address, + SigningGroupPubkeyX [32]uint8, + SigningGroupPubkeyY [32]uint8, + Timestamp *big.Int, + blockNumber uint64, + ) { + handler(DepositContractAddress.Hex()) + }, + func(err error) error { + return fmt.Errorf("watch deposit created failed: [%v]", err) + }, + nil, + ) +} + +// KeepAddress returns the underlying keep address for the +// provided deposit. +func (tec *TBTCEthereumChain) KeepAddress( + depositAddress string, +) (string, error) { + deposit, err := tec.getDepositContract(depositAddress) + if err != nil { + return "", err + } + + keepAddress, err := deposit.KeepAddress() + if err != nil { + return "", err + } + + return keepAddress.Hex(), nil +} + +// RetrieveSignerPubkey retrieves the signer public key for the +// provided deposit. +func (tec *TBTCEthereumChain) RetrieveSignerPubkey( + depositAddress string, +) error { + deposit, err := tec.getDepositContract(depositAddress) + if err != nil { + return err + } + + transaction, err := deposit.RetrieveSignerPubkey() + if err != nil { + return err + } + + logger.Debugf( + "submitted RetrieveSignerPubkey transaction with hash: [%x]", + transaction.Hash(), + ) + + return nil +} + +func (tec *TBTCEthereumChain) getDepositContract( + address string, +) (*contract.Deposit, error) { + if !common.IsHexAddress(address) { + return nil, fmt.Errorf("incorrect deposit contract address") + } + + depositContract, err := contract.NewDeposit( + common.HexToAddress(address), + tec.accountKey, + tec.client, + tec.nonceManager, + tec.miningWaiter, + tec.transactionMutex, + ) + if err != nil { + return nil, err + } + + return depositContract, nil +} diff --git a/pkg/chain/local/bonded_ecdsa_keep.go b/pkg/chain/local/bonded_ecdsa_keep.go index 7dfcbfc17..73c9e94cc 100644 --- a/pkg/chain/local/bonded_ecdsa_keep.go +++ b/pkg/chain/local/bonded_ecdsa_keep.go @@ -21,6 +21,9 @@ type localKeep struct { status keepStatus signatureRequestedHandlers map[int]func(event *eth.SignatureRequestedEvent) + + keepClosedHandlers map[int]func(event *eth.KeepClosedEvent) + keepTerminatedHandlers map[int]func(event *eth.KeepTerminatedEvent) } func (c *localChain) requestSignature(keepAddress common.Address, digest [32]byte) error { @@ -47,3 +50,67 @@ func (c *localChain) requestSignature(keepAddress common.Address, digest [32]byt return nil } + +func (c *localChain) closeKeep(keepAddress common.Address) error { + c.handlerMutex.Lock() + defer c.handlerMutex.Unlock() + + keep, ok := c.keeps[keepAddress] + if !ok { + return fmt.Errorf( + "failed to find keep with address: [%s]", + keepAddress.String(), + ) + } + + if keep.status != active { + return fmt.Errorf("only active keeps can be closed") + } + + keep.status = closed + + keepClosedEvent := ð.KeepClosedEvent{} + + for _, handler := range keep.keepClosedHandlers { + go func( + handler func(event *eth.KeepClosedEvent), + keepClosedEvent *eth.KeepClosedEvent, + ) { + handler(keepClosedEvent) + }(handler, keepClosedEvent) + } + + return nil +} + +func (c *localChain) terminateKeep(keepAddress common.Address) error { + c.handlerMutex.Lock() + defer c.handlerMutex.Unlock() + + keep, ok := c.keeps[keepAddress] + if !ok { + return fmt.Errorf( + "failed to find keep with address: [%s]", + keepAddress.String(), + ) + } + + if keep.status != active { + return fmt.Errorf("only active keeps can be terminated") + } + + keep.status = terminated + + keepTerminatedEvent := ð.KeepTerminatedEvent{} + + for _, handler := range keep.keepTerminatedHandlers { + go func( + handler func(event *eth.KeepTerminatedEvent), + keepTerminatedEvent *eth.KeepTerminatedEvent, + ) { + handler(keepTerminatedEvent) + }(handler, keepTerminatedEvent) + } + + return nil +} diff --git a/pkg/chain/local/bonded_ecdsa_keep_factory.go b/pkg/chain/local/bonded_ecdsa_keep_factory.go index 43c21f1c2..e0c756069 100644 --- a/pkg/chain/local/bonded_ecdsa_keep_factory.go +++ b/pkg/chain/local/bonded_ecdsa_keep_factory.go @@ -4,10 +4,17 @@ import ( "fmt" "github.com/ethereum/go-ethereum/common" - "github.com/keep-network/keep-ecdsa/pkg/chain" + chain "github.com/keep-network/keep-ecdsa/pkg/chain" ) func (c *localChain) createKeep(keepAddress common.Address) error { + return c.createKeepWithMembers(keepAddress, []common.Address{}) +} + +func (c *localChain) createKeepWithMembers( + keepAddress common.Address, + members []common.Address, +) error { c.handlerMutex.Lock() defer c.handlerMutex.Unlock() @@ -19,17 +26,25 @@ func (c *localChain) createKeep(keepAddress common.Address) error { } localKeep := &localKeep{ - signatureRequestedHandlers: make(map[int]func(event *eth.SignatureRequestedEvent)), publicKey: [64]byte{}, + members: members, + signatureRequestedHandlers: make(map[int]func(event *chain.SignatureRequestedEvent)), + keepClosedHandlers: make(map[int]func(event *chain.KeepClosedEvent)), + keepTerminatedHandlers: make(map[int]func(event *chain.KeepTerminatedEvent)), } + c.keeps[keepAddress] = localKeep + c.keepAddresses = append(c.keepAddresses, keepAddress) - keepCreatedEvent := ð.BondedECDSAKeepCreatedEvent{ + keepCreatedEvent := &chain.BondedECDSAKeepCreatedEvent{ KeepAddress: keepAddress, } for _, handler := range c.keepCreatedHandlers { - go func(handler func(event *eth.BondedECDSAKeepCreatedEvent), keepCreatedEvent *eth.BondedECDSAKeepCreatedEvent) { + go func( + handler func(event *chain.BondedECDSAKeepCreatedEvent), + keepCreatedEvent *chain.BondedECDSAKeepCreatedEvent, + ) { handler(keepCreatedEvent) }(handler, keepCreatedEvent) } diff --git a/pkg/chain/local/local.go b/pkg/chain/local/local.go index b5d644358..016d7e5e2 100644 --- a/pkg/chain/local/local.go +++ b/pkg/chain/local/local.go @@ -21,6 +21,7 @@ type Chain interface { OpenKeep(keepAddress common.Address, members []common.Address) CloseKeep(keepAddress common.Address) error + TerminateKeep(keepAddress common.Address) error AuthorizeOperator(operatorAddress common.Address) } @@ -53,25 +54,18 @@ func Connect() Chain { } func (lc *localChain) OpenKeep(keepAddress common.Address, members []common.Address) { - lc.handlerMutex.Lock() - defer lc.handlerMutex.Unlock() - - lc.keeps[keepAddress] = &localKeep{ - members: members, + err := lc.createKeepWithMembers(keepAddress, members) + if err != nil { + panic(err) } - lc.keepAddresses = append(lc.keepAddresses, keepAddress) } func (lc *localChain) CloseKeep(keepAddress common.Address) error { - lc.handlerMutex.Lock() - defer lc.handlerMutex.Unlock() + return lc.closeKeep(keepAddress) +} - keep, ok := lc.keeps[keepAddress] - if !ok { - return fmt.Errorf("no keep with address [%v]", keepAddress) - } - keep.status = closed - return nil +func (lc *localChain) TerminateKeep(keepAddress common.Address) error { + return lc.terminateKeep(keepAddress) } func (lc *localChain) AuthorizeOperator(operator common.Address) { @@ -255,14 +249,54 @@ func (lc *localChain) OnKeepClosed( keepAddress common.Address, handler func(event *eth.KeepClosedEvent), ) (subscription.EventSubscription, error) { - panic("implement") + lc.handlerMutex.Lock() + defer lc.handlerMutex.Unlock() + + handlerID := generateHandlerID() + + keep, ok := lc.keeps[keepAddress] + if !ok { + return nil, fmt.Errorf( + "failed to find keep with address: [%s]", + keepAddress.String(), + ) + } + + keep.keepClosedHandlers[handlerID] = handler + + return subscription.NewEventSubscription(func() { + lc.handlerMutex.Lock() + defer lc.handlerMutex.Unlock() + + delete(keep.keepClosedHandlers, handlerID) + }), nil } func (lc *localChain) OnKeepTerminated( keepAddress common.Address, handler func(event *eth.KeepTerminatedEvent), ) (subscription.EventSubscription, error) { - panic("implement") + lc.handlerMutex.Lock() + defer lc.handlerMutex.Unlock() + + handlerID := generateHandlerID() + + keep, ok := lc.keeps[keepAddress] + if !ok { + return nil, fmt.Errorf( + "failed to find keep with address: [%s]", + keepAddress.String(), + ) + } + + keep.keepTerminatedHandlers[handlerID] = handler + + return subscription.NewEventSubscription(func() { + lc.handlerMutex.Lock() + defer lc.handlerMutex.Unlock() + + delete(keep.keepTerminatedHandlers, handlerID) + }), nil } func (lc *localChain) OnConflictingPublicKeySubmitted( @@ -322,3 +356,13 @@ func generateHandlerID() int { // Local chain implementation doesn't require secure randomness. return rand.Int() } + +func generateAddress() common.Address { + var address [20]byte + // #nosec G404 G104 (insecure random number source (rand) | error unhandled) + // Local chain implementation doesn't require secure randomness. + // Error can be ignored because according to the `rand.Read` docs it's + // always `nil`. + rand.Read(address[:]) + return address +} diff --git a/pkg/chain/local/tbtc.go b/pkg/chain/local/tbtc.go new file mode 100644 index 000000000..c3e6d2198 --- /dev/null +++ b/pkg/chain/local/tbtc.go @@ -0,0 +1,193 @@ +package local + +import ( + "bytes" + "fmt" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/keep-network/keep-common/pkg/subscription" +) + +type localDeposit struct { + keepAddress string + pubkey []byte +} + +type localChainLogger struct { + retrieveSignerPubkeyCalls int +} + +func (lcl *localChainLogger) logRetrieveSignerPubkeyCall() { + lcl.retrieveSignerPubkeyCalls++ +} + +func (lcl *localChainLogger) RetrieveSignerPubkeyCalls() int { + return lcl.retrieveSignerPubkeyCalls +} + +type TBTCLocalChain struct { + *localChain + + mutex sync.Mutex + + logger *localChainLogger + + deposits map[string]*localDeposit + depositCreatedHandlers map[int]func(depositAddress string) + depositRegisteredPubkeyHandlers map[int]func(depositAddress string) +} + +func NewTBTCLocalChain() *TBTCLocalChain { + return &TBTCLocalChain{ + localChain: Connect().(*localChain), + logger: &localChainLogger{}, + deposits: make(map[string]*localDeposit), + depositCreatedHandlers: make(map[int]func(depositAddress string)), + depositRegisteredPubkeyHandlers: make(map[int]func(depositAddress string)), + } +} + +func (tlc *TBTCLocalChain) CreateDeposit(depositAddress string) { + tlc.mutex.Lock() + defer tlc.mutex.Unlock() + + keepAddress := generateAddress() + tlc.OpenKeep(keepAddress, []common.Address{ + generateAddress(), + generateAddress(), + generateAddress(), + }) + + tlc.deposits[depositAddress] = &localDeposit{ + keepAddress: keepAddress.Hex(), + } + + for _, handler := range tlc.depositCreatedHandlers { + go func(handler func(depositAddress string), depositAddress string) { + handler(depositAddress) + }(handler, depositAddress) + } +} + +func (tlc *TBTCLocalChain) OnDepositCreated( + handler func(depositAddress string), +) (subscription.EventSubscription, error) { + tlc.mutex.Lock() + defer tlc.mutex.Unlock() + + handlerID := generateHandlerID() + + tlc.depositCreatedHandlers[handlerID] = handler + + return subscription.NewEventSubscription(func() { + tlc.mutex.Lock() + defer tlc.mutex.Unlock() + + delete(tlc.depositCreatedHandlers, handlerID) + }), nil +} + +func (tlc *TBTCLocalChain) OnDepositRegisteredPubkey( + handler func(depositAddress string), +) (subscription.EventSubscription, error) { + tlc.mutex.Lock() + defer tlc.mutex.Unlock() + + handlerID := generateHandlerID() + + tlc.depositRegisteredPubkeyHandlers[handlerID] = handler + + return subscription.NewEventSubscription(func() { + tlc.mutex.Lock() + defer tlc.mutex.Unlock() + + delete(tlc.depositRegisteredPubkeyHandlers, handlerID) + }), nil +} + +func (tlc *TBTCLocalChain) KeepAddress(depositAddress string) (string, error) { + tlc.mutex.Lock() + defer tlc.mutex.Unlock() + + deposit, ok := tlc.deposits[depositAddress] + if !ok { + return "", fmt.Errorf("no deposit with address [%v]", depositAddress) + } + + return deposit.keepAddress, nil +} + +func (tlc *TBTCLocalChain) RetrieveSignerPubkey(depositAddress string) error { + tlc.mutex.Lock() + defer tlc.mutex.Unlock() + + tlc.logger.logRetrieveSignerPubkeyCall() + + deposit, ok := tlc.deposits[depositAddress] + if !ok { + return fmt.Errorf("no deposit with address [%v]", depositAddress) + } + + if len(deposit.pubkey) > 0 { + return fmt.Errorf( + "pubkey for deposit [%v] already retrieved", + depositAddress, + ) + } + + // lock upstream mutex to access `keeps` map safely + tlc.handlerMutex.Lock() + defer tlc.handlerMutex.Unlock() + + keep, ok := tlc.keeps[common.HexToAddress(deposit.keepAddress)] + if !ok { + return fmt.Errorf( + "could not find keep for deposit [%v]", + depositAddress, + ) + } + + if len(keep.publicKey[:]) == 0 || + bytes.Equal(keep.publicKey[:], make([]byte, len(keep.publicKey))) { + return fmt.Errorf( + "keep of deposit [%v] doesn't have a public key yet", + depositAddress, + ) + } + + deposit.pubkey = keep.publicKey[:] + + for _, handler := range tlc.depositRegisteredPubkeyHandlers { + go func(handler func(depositAddress string), depositAddress string) { + handler(depositAddress) + }(handler, depositAddress) + } + + return nil +} + +func (tlc *TBTCLocalChain) DepositPubkey( + depositAddress string, +) ([]byte, error) { + tlc.mutex.Lock() + defer tlc.mutex.Unlock() + + deposit, ok := tlc.deposits[depositAddress] + if !ok { + return nil, fmt.Errorf("no deposit with address [%v]", depositAddress) + } + + if len(deposit.pubkey) == 0 { + return nil, fmt.Errorf( + "no pubkey for deposit [%v]", + depositAddress, + ) + } + + return deposit.pubkey, nil +} + +func (tlc *TBTCLocalChain) Logger() *localChainLogger { + return tlc.logger +} diff --git a/pkg/extensions/tbtc/chain.go b/pkg/extensions/tbtc/chain.go new file mode 100644 index 000000000..598814880 --- /dev/null +++ b/pkg/extensions/tbtc/chain.go @@ -0,0 +1,42 @@ +package tbtc + +import ( + "github.com/keep-network/keep-common/pkg/subscription" + chain "github.com/keep-network/keep-ecdsa/pkg/chain" +) + +// Handle represents a chain handle extended with TBTC-specific capabilities. +type Handle interface { + chain.Handle + + Deposit + TBTCSystem +} + +// Deposit is an interface that provides ability to interact +// with Deposit contracts. +type Deposit interface { + // KeepAddress returns the underlying keep address for the + // provided deposit. + KeepAddress(depositAddress string) (string, error) + + // RetrieveSignerPubkey retrieves the signer public key for the + // provided deposit. + RetrieveSignerPubkey(depositAddress string) error +} + +// TBTCSystem is an interface that provides ability to interact +// with TBTCSystem contract. +type TBTCSystem interface { + // OnDepositCreated installs a callback that is invoked when an + // on-chain notification of a new deposit creation is seen. + OnDepositCreated( + handler func(depositAddress string), + ) (subscription.EventSubscription, error) + + // OnDepositRegisteredPubkey installs a callback that is invoked when an + // on-chain notification of a deposit's pubkey registration is seen. + OnDepositRegisteredPubkey( + handler func(depositAddress string), + ) (subscription.EventSubscription, error) +} diff --git a/pkg/extensions/tbtc/tbtc.go b/pkg/extensions/tbtc/tbtc.go new file mode 100644 index 000000000..bf88c0f06 --- /dev/null +++ b/pkg/extensions/tbtc/tbtc.go @@ -0,0 +1,292 @@ +package tbtc + +import ( + "context" + "fmt" + "math" + "math/rand" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/ipfs/go-log" + "github.com/keep-network/keep-common/pkg/subscription" + chain "github.com/keep-network/keep-ecdsa/pkg/chain" +) + +var logger = log.Logger("tbtc-extension") + +const maxActAttempts = 3 + +// Initialize initializes extension specific to the TBTC application. +func Initialize(ctx context.Context, handle Handle) error { + logger.Infof("initializing tbtc extension") + + tbtc := &tbtc{handle} + + err := tbtc.monitorRetrievePubKey( + ctx, + exponentialBackoff, + 150*time.Minute, + ) + if err != nil { + return fmt.Errorf( + "could not initialize retrieve pubkey monitoring: [%v]", + err, + ) + } + + logger.Infof("tbtc extension has been initialized") + + return nil +} + +type depositEventHandler func(deposit string) + +type watchDepositEventFn func( + handler depositEventHandler, +) (subscription.EventSubscription, error) + +type watchKeepClosedFn func(deposit string) ( + keepClosedChan chan struct{}, + unsubscribe func(), + err error, +) + +type submitDepositTxFn func(deposit string) error + +type backoffFn func(iteration int) time.Duration + +type tbtc struct { + chain Handle +} + +func (t *tbtc) monitorRetrievePubKey( + ctx context.Context, + actBackoffFn backoffFn, + timeout time.Duration, +) error { + monitoringSubscription, err := t.monitorAndAct( + ctx, + "retrieve pubkey", + func(handler depositEventHandler) (subscription.EventSubscription, error) { + return t.chain.OnDepositCreated(handler) + }, + func(handler depositEventHandler) (subscription.EventSubscription, error) { + return t.chain.OnDepositRegisteredPubkey(handler) + }, + t.watchKeepClosed, + func(deposit string) error { + return t.chain.RetrieveSignerPubkey(deposit) + }, + actBackoffFn, + timeout, + ) + if err != nil { + return err + } + + go func() { + <-ctx.Done() + monitoringSubscription.Unsubscribe() + logger.Infof("retrieve pubkey monitoring disabled") + }() + + logger.Infof("retrieve pubkey monitoring initialized") + + return nil +} + +// TODO: +// 1. Filter incoming events by operator interest. +// 2. Incoming events deduplication. +// 3. Resume monitoring after client restart. +func (t *tbtc) monitorAndAct( + ctx context.Context, + monitoringName string, + monitoringStartFn watchDepositEventFn, + monitoringStopFn watchDepositEventFn, + keepClosedFn watchKeepClosedFn, + actFn submitDepositTxFn, + actBackoffFn backoffFn, + timeout time.Duration, +) (subscription.EventSubscription, error) { + handleStartEvent := func(deposit string) { + logger.Infof( + "starting [%v] monitoring for deposit [%v]", + monitoringName, + deposit, + ) + + stopEventChan := make(chan struct{}) + + stopEventSubscription, err := monitoringStopFn( + func(stopEventDeposit string) { + if deposit == stopEventDeposit { + stopEventChan <- struct{}{} + } + }, + ) + if err != nil { + logger.Errorf( + "could not setup stop event handler for [%v] "+ + "monitoring for deposit [%v]: [%v]", + monitoringName, + deposit, + err, + ) + return + } + defer stopEventSubscription.Unsubscribe() + + keepClosedChan, keepClosedUnsubscribe, err := keepClosedFn(deposit) + if err != nil { + logger.Errorf( + "could not setup keep closed handler for [%v] "+ + "monitoring for deposit [%v]: [%v]", + monitoringName, + deposit, + err, + ) + return + } + defer keepClosedUnsubscribe() + + timeoutChan := time.After(timeout) + + actionAttempt := 1 + + monitoring: + for { + select { + case <-ctx.Done(): + logger.Infof( + "context is done for [%v] "+ + "monitoring for deposit [%v]", + monitoringName, + deposit, + ) + break monitoring + case <-stopEventChan: + logger.Infof( + "stop event occurred for [%v] "+ + "monitoring for deposit [%v]", + monitoringName, + deposit, + ) + break monitoring + case <-keepClosedChan: + logger.Infof( + "keep closed event occurred for [%v] "+ + "monitoring for deposit [%v]", + monitoringName, + deposit, + ) + break monitoring + case <-timeoutChan: + logger.Infof( + "[%v] not performed in the expected time frame "+ + "for deposit [%v]; performing the action", + monitoringName, + deposit, + ) + + err := actFn(deposit) + if err != nil { + if actionAttempt == maxActAttempts { + logger.Errorf( + "could not perform action "+ + "for [%v] monitoring for deposit [%v]: [%v]; "+ + "the maximum number of attempts reached", + monitoringName, + deposit, + err, + ) + break monitoring + } + + backoff := actBackoffFn(actionAttempt) + + logger.Errorf( + "could not perform action "+ + "for [%v] monitoring for deposit [%v]: [%v]; "+ + "retrying after: [%v]", + monitoringName, + deposit, + err, + backoff, + ) + + timeoutChan = time.After(backoff) + actionAttempt++ + } else { + break monitoring + } + } + } + + logger.Infof( + "stopped [%v] monitoring for deposit [%v]", + monitoringName, + deposit, + ) + } + + return monitoringStartFn( + func(deposit string) { + go handleStartEvent(deposit) + }, + ) +} + +func (t *tbtc) watchKeepClosed( + deposit string, +) (chan struct{}, func(), error) { + signalChan := make(chan struct{}) + + keepAddress, err := t.chain.KeepAddress(deposit) + if err != nil { + return nil, nil, err + } + + keepClosedSubscription, err := t.chain.OnKeepClosed( + common.HexToAddress(keepAddress), + func(_ *chain.KeepClosedEvent) { + signalChan <- struct{}{} + }, + ) + if err != nil { + return nil, nil, err + } + + keepTerminatedSubscription, err := t.chain.OnKeepTerminated( + common.HexToAddress(keepAddress), + func(_ *chain.KeepTerminatedEvent) { + signalChan <- struct{}{} + }, + ) + if err != nil { + return nil, nil, err + } + + unsubscribe := func() { + keepClosedSubscription.Unsubscribe() + keepTerminatedSubscription.Unsubscribe() + } + + return signalChan, unsubscribe, nil +} + +// Computes the exponential backoff value for given iteration. +// For each iteration the result value will be in range: +// - iteration 1: [2000ms, 2100ms) +// - iteration 2: [4000ms, 4100ms) +// - iteration 3: [8000ms, 8100ms) +// - iteration n: [2^n * 1000ms, (2^n * 1000ms) + 100ms) +func exponentialBackoff(iteration int) time.Duration { + backoffMillis := math.Pow(2, float64(iteration)) * 1000 + // #nosec G404 (insecure random number source (rand)) + // No need to use secure randomness for jitter value. + jitterMillis := rand.Intn(100) + return time.Duration(int(backoffMillis)+jitterMillis) * time.Millisecond +} diff --git a/pkg/extensions/tbtc/tbtc_test.go b/pkg/extensions/tbtc/tbtc_test.go new file mode 100644 index 000000000..bd2bb63fb --- /dev/null +++ b/pkg/extensions/tbtc/tbtc_test.go @@ -0,0 +1,429 @@ +package tbtc + +import ( + "bytes" + "context" + "fmt" + "math/rand" + "reflect" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/keep-network/keep-ecdsa/pkg/chain/local" +) + +const ( + timeout = 500 * time.Millisecond + depositAddress = "0xa5FA806723A7c7c8523F33c39686f20b52612877" +) + +func TestRetrievePubkey_TimeoutElapsed(t *testing.T) { + ctx := context.Background() + tbtcChain := local.NewTBTCLocalChain() + tbtc := &tbtc{tbtcChain} + + err := tbtc.monitorRetrievePubKey( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.CreateDeposit(depositAddress) + + keepPubkey, err := submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + // wait a bit longer than the monitoring timeout + // to make sure the potential transaction completes + time.Sleep(2 * timeout) + + expectedRetrieveSignerPubkeyCalls := 1 + actualRetrieveSignerPubkeyCalls := tbtcChain.Logger().RetrieveSignerPubkeyCalls() + if expectedRetrieveSignerPubkeyCalls != actualRetrieveSignerPubkeyCalls { + t.Errorf( + "unexpected number of RetrieveSignerPubkey calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedRetrieveSignerPubkeyCalls, + actualRetrieveSignerPubkeyCalls, + ) + } + + depositPubkey, err := tbtcChain.DepositPubkey(depositAddress) + if err != nil { + t.Errorf("unexpected error while fetching deposit pubkey: [%v]", err) + } + + if !bytes.Equal(keepPubkey[:], depositPubkey) { + t.Errorf( + "unexpected public key\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + keepPubkey, + depositPubkey, + ) + } +} + +func TestRetrievePubkey_StopEventOccurred(t *testing.T) { + ctx := context.Background() + tbtcChain := local.NewTBTCLocalChain() + tbtc := &tbtc{tbtcChain} + + err := tbtc.monitorRetrievePubKey( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.CreateDeposit(depositAddress) + + keepPubkey, err := submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + // wait a while before triggering the stop event because the + // extension must have time to handle the start event + time.Sleep(100 * time.Millisecond) + + // invoke the action which will trigger the stop event in result + err = tbtcChain.RetrieveSignerPubkey(depositAddress) + if err != nil { + t.Fatal(err) + } + + // wait a bit longer than the monitoring timeout + // to make sure the potential transaction completes + time.Sleep(2 * timeout) + + expectedRetrieveSignerPubkeyCalls := 1 + actualRetrieveSignerPubkeyCalls := tbtcChain.Logger().RetrieveSignerPubkeyCalls() + if expectedRetrieveSignerPubkeyCalls != actualRetrieveSignerPubkeyCalls { + t.Errorf( + "unexpected number of RetrieveSignerPubkey calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedRetrieveSignerPubkeyCalls, + actualRetrieveSignerPubkeyCalls, + ) + } + + depositPubkey, err := tbtcChain.DepositPubkey(depositAddress) + if err != nil { + t.Errorf("unexpected error while fetching deposit pubkey: [%v]", err) + } + + if !bytes.Equal(keepPubkey[:], depositPubkey) { + t.Errorf( + "unexpected public key\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + keepPubkey, + depositPubkey, + ) + } +} + +func TestRetrievePubkey_KeepClosedEventOccurred(t *testing.T) { + ctx := context.Background() + tbtcChain := local.NewTBTCLocalChain() + tbtc := &tbtc{tbtcChain} + + err := tbtc.monitorRetrievePubKey( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.CreateDeposit(depositAddress) + + _, err = submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + // wait a while before triggering the keep closed event because the + // extension must have time to handle the start event + time.Sleep(100 * time.Millisecond) + + err = closeKeep(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + // wait a bit longer than the monitoring timeout + // to make sure the potential transaction completes + time.Sleep(2 * timeout) + + expectedRetrieveSignerPubkeyCalls := 0 + actualRetrieveSignerPubkeyCalls := tbtcChain.Logger().RetrieveSignerPubkeyCalls() + if expectedRetrieveSignerPubkeyCalls != actualRetrieveSignerPubkeyCalls { + t.Errorf( + "unexpected number of RetrieveSignerPubkey calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedRetrieveSignerPubkeyCalls, + actualRetrieveSignerPubkeyCalls, + ) + } + + _, err = tbtcChain.DepositPubkey(depositAddress) + + expectedError := fmt.Errorf("no pubkey for deposit [%v]", depositAddress) + if !reflect.DeepEqual(expectedError, err) { + t.Errorf( + "unexpected error\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedError, + err, + ) + } +} + +func TestRetrievePubkey_KeepTerminatedEventOccurred(t *testing.T) { + ctx := context.Background() + tbtcChain := local.NewTBTCLocalChain() + tbtc := &tbtc{tbtcChain} + + err := tbtc.monitorRetrievePubKey( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.CreateDeposit(depositAddress) + + _, err = submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + // wait a while before triggering the keep terminated event because the + // extension must have time to handle the start event + time.Sleep(100 * time.Millisecond) + + err = terminateKeep(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + // wait a bit longer than the monitoring timeout + // to make sure the potential transaction completes + time.Sleep(2 * timeout) + + expectedRetrieveSignerPubkeyCalls := 0 + actualRetrieveSignerPubkeyCalls := tbtcChain.Logger().RetrieveSignerPubkeyCalls() + if expectedRetrieveSignerPubkeyCalls != actualRetrieveSignerPubkeyCalls { + t.Errorf( + "unexpected number of RetrieveSignerPubkey calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedRetrieveSignerPubkeyCalls, + actualRetrieveSignerPubkeyCalls, + ) + } + + _, err = tbtcChain.DepositPubkey(depositAddress) + + expectedError := fmt.Errorf("no pubkey for deposit [%v]", depositAddress) + if !reflect.DeepEqual(expectedError, err) { + t.Errorf( + "unexpected error\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedError, + err, + ) + } +} + +func TestRetrievePubkey_ActionFailed(t *testing.T) { + ctx := context.Background() + tbtcChain := local.NewTBTCLocalChain() + tbtc := &tbtc{tbtcChain} + + err := tbtc.monitorRetrievePubKey( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.CreateDeposit(depositAddress) + + // do not submit the keep public key intentionally to cause + // the action error + + // wait a bit longer than the monitoring timeout + // to make sure the potential transaction completes + time.Sleep(2 * timeout) + + expectedRetrieveSignerPubkeyCalls := 3 + actualRetrieveSignerPubkeyCalls := tbtcChain.Logger().RetrieveSignerPubkeyCalls() + if expectedRetrieveSignerPubkeyCalls != actualRetrieveSignerPubkeyCalls { + t.Errorf( + "unexpected number of RetrieveSignerPubkey calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedRetrieveSignerPubkeyCalls, + actualRetrieveSignerPubkeyCalls, + ) + } +} + +func TestRetrievePubkey_ContextCancelled_WithoutWorkingMonitoring(t *testing.T) { + ctx, cancelCtx := context.WithCancel(context.Background()) + tbtcChain := local.NewTBTCLocalChain() + tbtc := &tbtc{tbtcChain} + + err := tbtc.monitorRetrievePubKey( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + // cancel the context before any start event occurs + cancelCtx() + + tbtcChain.CreateDeposit(depositAddress) + + // wait a bit longer than the monitoring timeout + // to make sure the potential transaction completes + time.Sleep(2 * timeout) + + expectedRetrieveSignerPubkeyCalls := 0 + actualRetrieveSignerPubkeyCalls := tbtcChain.Logger().RetrieveSignerPubkeyCalls() + if expectedRetrieveSignerPubkeyCalls != actualRetrieveSignerPubkeyCalls { + t.Errorf( + "unexpected number of RetrieveSignerPubkey calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedRetrieveSignerPubkeyCalls, + actualRetrieveSignerPubkeyCalls, + ) + } +} + +func TestRetrievePubkey_ContextCancelled_WithWorkingMonitoring(t *testing.T) { + ctx, cancelCtx := context.WithCancel(context.Background()) + tbtcChain := local.NewTBTCLocalChain() + tbtc := &tbtc{tbtcChain} + + err := tbtc.monitorRetrievePubKey( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.CreateDeposit(depositAddress) + + // wait a while before cancelling the context because the + // extension must have time to handle the start event + time.Sleep(100 * time.Millisecond) + + // cancel the context once the start event is handled and + // the monitoring process is running + cancelCtx() + + // wait a bit longer than the monitoring timeout + // to make sure the potential transaction completes + time.Sleep(2 * timeout) + + expectedRetrieveSignerPubkeyCalls := 0 + actualRetrieveSignerPubkeyCalls := tbtcChain.Logger().RetrieveSignerPubkeyCalls() + if expectedRetrieveSignerPubkeyCalls != actualRetrieveSignerPubkeyCalls { + t.Errorf( + "unexpected number of RetrieveSignerPubkey calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedRetrieveSignerPubkeyCalls, + actualRetrieveSignerPubkeyCalls, + ) + } +} + +func submitKeepPublicKey( + depositAddress string, + tbtcChain *local.TBTCLocalChain, +) ([64]byte, error) { + keepAddress, err := tbtcChain.KeepAddress(depositAddress) + if err != nil { + return [64]byte{}, err + } + + var keepPubkey [64]byte + rand.Read(keepPubkey[:]) + + err = tbtcChain.SubmitKeepPublicKey( + common.HexToAddress(keepAddress), + keepPubkey, + ) + if err != nil { + return [64]byte{}, err + } + + return keepPubkey, nil +} + +func closeKeep( + depositAddress string, + tbtcChain *local.TBTCLocalChain, +) error { + keepAddress, err := tbtcChain.KeepAddress(depositAddress) + if err != nil { + return err + } + + err = tbtcChain.CloseKeep(common.HexToAddress(keepAddress)) + if err != nil { + return err + } + + return nil +} + +func terminateKeep( + depositAddress string, + tbtcChain *local.TBTCLocalChain, +) error { + keepAddress, err := tbtcChain.KeepAddress(depositAddress) + if err != nil { + return err + } + + err = tbtcChain.TerminateKeep(common.HexToAddress(keepAddress)) + if err != nil { + return err + } + + return nil +} + +func constantBackoff(_ int) time.Duration { + return time.Millisecond +}