diff --git a/go.mod b/go.mod index 46c3db432..139d4d6e7 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/ipfs/go-log v1.0.4 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/keep-network/tbtc v1.1.1-0.20201026093513-cb9246987718 github.com/pkg/errors v0.9.1 github.com/urfave/cli v1.22.1 ) diff --git a/go.sum b/go.sum index e89cf4f9e..226695f3d 100644 --- a/go.sum +++ b/go.sum @@ -337,8 +337,8 @@ github.com/keep-network/keep-common v1.2.1-0.20201020114759-19c123cbd4f4 h1:Civu 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/tbtc v1.1.1-0.20201026093513-cb9246987718 h1:/ZNMBY7y6hfzCYA8mgtHnspGO26OmWV3sDehyGnqRyY= +github.com/keep-network/tbtc v1.1.1-0.20201026093513-cb9246987718/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/pkg/chain/chain.go b/pkg/chain/chain.go index e80dbac26..5036c7bd4 100644 --- a/pkg/chain/chain.go +++ b/pkg/chain/chain.go @@ -20,6 +20,9 @@ type Handle interface { StakeMonitor() (chain.StakeMonitor, error) // BlockCounter returns a block counter. BlockCounter() chain.BlockCounter + // BlockTimestamp returns given block's timestamp. + // In case the block is not yet mined, an error should be returned. + BlockTimestamp(blockNumber *big.Int) (uint64, error) BondedECDSAKeepFactory BondedECDSAKeep @@ -143,4 +146,13 @@ type BondedECDSAKeep interface { // GetOpenedTimestamp returns timestamp when the keep was created. GetOpenedTimestamp(keepAddress common.Address) (time.Time, error) + + // PastSignatureSubmittedEvents returns all signature submitted events + // for the given keep which occurred after the provided start block. + // All implementations should returns those events sorted by the + // block number in the ascending order. + PastSignatureSubmittedEvents( + keepAddress string, + startBlock uint64, + ) ([]*SignatureSubmittedEvent, error) } diff --git a/pkg/chain/ethereum/connect.go b/pkg/chain/ethereum/connect.go index 5a7e059f2..830216269 100644 --- a/pkg/chain/ethereum/connect.go +++ b/pkg/chain/ethereum/connect.go @@ -1,6 +1,7 @@ package ethereum import ( + "context" "fmt" "math/big" "sync" @@ -41,6 +42,7 @@ type EthereumChain struct { blockCounter *blockcounter.EthereumBlockCounter miningWaiter *ethutil.MiningWaiter nonceManager *ethutil.NonceManager + blockTimestampFn blockTimestampFn // transactionMutex allows interested parties to forcibly serialize // transaction submission. @@ -65,6 +67,10 @@ func Connect(accountKey *keystore.Key, config *ethereum.Config) (*EthereumChain, return nil, err } + // TODO: Add rate limiting to (keep-ecdsa/pull/585#discussion_r513351032): + // - `createBlockTimestampFn` + // - `blockCounter` + // - `miningWaiter` wrappedClient := addClientWrappers(config, client) transactionMutex := &sync.Mutex{} @@ -120,6 +126,7 @@ func Connect(accountKey *keystore.Key, config *ethereum.Config) (*EthereumChain, nonceManager: nonceManager, miningWaiter: miningWaiter, transactionMutex: transactionMutex, + blockTimestampFn: createBlockTimestampFn(client), }, nil } @@ -149,3 +156,19 @@ func addClientWrappers( return loggingBackend } + +type blockTimestampFn func(blockNumber *big.Int) (uint64, error) + +func createBlockTimestampFn(client *ethclient.Client) blockTimestampFn { + return func(blockNumber *big.Int) (uint64, error) { + ctx, cancelCtx := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancelCtx() + + header, err := client.HeaderByNumber(ctx, blockNumber) + if err != nil { + return 0, err + } + + return header.Time, nil + } +} diff --git a/pkg/chain/ethereum/ethereum.go b/pkg/chain/ethereum/ethereum.go index 92cc1311e..0e1db077a 100644 --- a/pkg/chain/ethereum/ethereum.go +++ b/pkg/chain/ethereum/ethereum.go @@ -4,8 +4,11 @@ package ethereum import ( "fmt" "math/big" + "sort" "time" + "github.com/keep-network/keep-ecdsa/pkg/chain/gen/eventlog" + "github.com/ethereum/go-ethereum/common" "github.com/ipfs/go-log" @@ -507,3 +510,55 @@ func (ec *EthereumChain) GetOpenedTimestamp(keepAddress common.Address) (time.Ti return keepOpenTime, nil } + +// PastSignatureSubmittedEvents returns all signature submitted events +// for the given keep which occurred after the provided start block. +// Returned events are sorted by the block number in the ascending order. +func (ec *EthereumChain) PastSignatureSubmittedEvents( + keepAddress string, + startBlock uint64, +) ([]*eth.SignatureSubmittedEvent, error) { + if !common.IsHexAddress(keepAddress) { + return nil, fmt.Errorf("invalid keep address: [%v]", keepAddress) + } + + keepContractEventLog, err := eventlog.NewBondedECDSAKeepEventLog( + common.HexToAddress(keepAddress), + ec.client, + ) + if err != nil { + return nil, err + } + + events, err := keepContractEventLog.PastSignatureSubmittedEvents( + startBlock, + nil, + ) + if err != nil { + return nil, err + } + + result := make([]*eth.SignatureSubmittedEvent, 0) + + for _, event := range events { + result = append(result, ð.SignatureSubmittedEvent{ + Digest: event.Digest, + R: event.R, + S: event.S, + RecoveryID: event.RecoveryID, + BlockNumber: event.BlockNumber, + }) + } + + // Make sure events are sorted by block number in ascending order. + sort.SliceStable(result, func(i, j int) bool { + return result[i].BlockNumber < result[j].BlockNumber + }) + + return result, nil +} + +// BlockTimestamp returns given block's timestamp. +func (ec *EthereumChain) BlockTimestamp(blockNumber *big.Int) (uint64, error) { + return ec.blockTimestampFn(blockNumber) +} diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index a61798240..e681c3c7a 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -3,10 +3,14 @@ package ethereum import ( "fmt" "math/big" + "sort" + + chain "github.com/keep-network/keep-ecdsa/pkg/chain" "github.com/ethereum/go-ethereum/common" "github.com/keep-network/keep-common/pkg/subscription" "github.com/keep-network/tbtc/pkg/chain/ethereum/gen/contract" + "github.com/keep-network/tbtc/pkg/chain/ethereum/gen/eventlog" ) // TBTCEthereumChain represents an Ethereum chain handle with @@ -15,6 +19,7 @@ type TBTCEthereumChain struct { *EthereumChain tbtcSystemContract *contract.TBTCSystem + tbtcSystemEventLog *eventlog.TBTCSystemEventLog } // WithTBTCExtension extends the Ethereum chain handle with @@ -39,9 +44,18 @@ func WithTBTCExtension( return nil, err } + tbtcSystemEventLog, err := eventlog.NewTBTCSystemEventLog( + common.HexToAddress(tbtcSystemContractAddress), + ethereumChain.client, + ) + if err != nil { + return nil, err + } + return &TBTCEthereumChain{ EthereumChain: ethereumChain, tbtcSystemContract: tbtcSystemContract, + tbtcSystemEventLog: tbtcSystemEventLog, }, nil } @@ -83,12 +97,142 @@ func (tec *TBTCEthereumChain) OnDepositRegisteredPubkey( handler(DepositContractAddress.Hex()) }, func(err error) error { - return fmt.Errorf("watch deposit created failed: [%v]", err) + return fmt.Errorf( + "watch deposit registered pubkey failed: [%v]", + err, + ) + }, + nil, + ) +} + +// OnDepositRedemptionRequested installs a callback that is invoked when an +// on-chain notification of a deposit redemption request is seen. +func (tec *TBTCEthereumChain) OnDepositRedemptionRequested( + handler func(depositAddress string), +) (subscription.EventSubscription, error) { + return tec.tbtcSystemContract.WatchRedemptionRequested( + func( + DepositContractAddress common.Address, + Requester common.Address, + Digest [32]uint8, + UtxoValue *big.Int, + RedeemerOutputScript []uint8, + RequestedFee *big.Int, + Outpoint []uint8, + blockNumber uint64, + ) { + handler(DepositContractAddress.Hex()) + }, + func(err error) error { + return fmt.Errorf( + "watch deposit redemption requested failed: [%v]", + err, + ) + }, + nil, + nil, + nil, + ) +} + +// OnDepositGotRedemptionSignature installs a callback that is invoked when an +// on-chain notification of a deposit receiving a redemption signature is seen. +func (tec *TBTCEthereumChain) OnDepositGotRedemptionSignature( + handler func(depositAddress string), +) (subscription.EventSubscription, error) { + return tec.tbtcSystemContract.WatchGotRedemptionSignature( + func( + DepositContractAddress common.Address, + Digest [32]uint8, + R [32]uint8, + S [32]uint8, + Timestamp *big.Int, + blockNumber uint64, + ) { + handler(DepositContractAddress.Hex()) + }, + func(err error) error { + return fmt.Errorf( + "watch deposit got redemption signature failed: [%v]", + err, + ) }, nil, + nil, ) } +// OnDepositRedeemed installs a callback that is invoked when an +// on-chain notification of a deposit redemption is seen. +func (tec *TBTCEthereumChain) OnDepositRedeemed( + handler func(depositAddress string), +) (subscription.EventSubscription, error) { + return tec.tbtcSystemContract.WatchRedeemed( + func( + DepositContractAddress common.Address, + Txid [32]uint8, + Timestamp *big.Int, + blockNumber uint64, + ) { + handler(DepositContractAddress.Hex()) + }, + func(err error) error { + return fmt.Errorf( + "watch deposit redeemed failed: [%v]", + err, + ) + }, + nil, + nil, + ) +} + +// PastDepositRedemptionRequestedEvents returns all redemption requested +// events for the given deposit which occurred after the provided start block. +// Returned events are sorted by the block number in the ascending order. +func (tec *TBTCEthereumChain) PastDepositRedemptionRequestedEvents( + depositAddress string, + startBlock uint64, +) ([]*chain.DepositRedemptionRequestedEvent, error) { + if !common.IsHexAddress(depositAddress) { + return nil, fmt.Errorf("incorrect deposit contract address") + } + + events, err := tec.tbtcSystemEventLog.PastRedemptionRequestedEvents( + []common.Address{ + common.HexToAddress(depositAddress), + }, + startBlock, + nil, + ) + if err != nil { + return nil, err + } + + result := make([]*chain.DepositRedemptionRequestedEvent, 0) + + for _, event := range events { + result = append(result, &chain.DepositRedemptionRequestedEvent{ + DepositAddress: event.DepositContractAddress.Hex(), + RequesterAddress: event.Requester.Hex(), + Digest: event.Digest, + UtxoValue: event.UtxoValue, + RedeemerOutputScript: event.RedeemerOutputScript, + RequestedFee: event.RequestedFee, + Outpoint: event.Outpoint, + BlockNumber: event.BlockNumber, + }) + } + + // Make sure events are sorted by block number in ascending order. + sort.SliceStable(result, func(i, j int) bool { + return result[i].BlockNumber < result[j].BlockNumber + }) + + return result, nil +} + // KeepAddress returns the underlying keep address for the // provided deposit. func (tec *TBTCEthereumChain) KeepAddress( @@ -130,6 +274,96 @@ func (tec *TBTCEthereumChain) RetrieveSignerPubkey( return nil } +// ProvideRedemptionSignature provides the redemption signature for the +// provided deposit. +func (tec *TBTCEthereumChain) ProvideRedemptionSignature( + depositAddress string, + v uint8, + r [32]uint8, + s [32]uint8, +) error { + deposit, err := tec.getDepositContract(depositAddress) + if err != nil { + return err + } + + transaction, err := deposit.ProvideRedemptionSignature(v, r, s) + if err != nil { + return err + } + + logger.Debugf( + "submitted ProvideRedemptionSignature transaction with hash: [%x]", + transaction.Hash(), + ) + + return nil +} + +// IncreaseRedemptionFee increases the redemption fee for the provided deposit. +func (tec *TBTCEthereumChain) IncreaseRedemptionFee( + depositAddress string, + previousOutputValueBytes [8]uint8, + newOutputValueBytes [8]uint8, +) error { + deposit, err := tec.getDepositContract(depositAddress) + if err != nil { + return err + } + + transaction, err := deposit.IncreaseRedemptionFee( + previousOutputValueBytes, + newOutputValueBytes, + ) + if err != nil { + return err + } + + logger.Debugf( + "submitted IncreaseRedemptionFee transaction with hash: [%x]", + transaction.Hash(), + ) + + return nil +} + +// ProvideRedemptionProof provides the redemption proof for the provided deposit. +func (tec *TBTCEthereumChain) ProvideRedemptionProof( + depositAddress string, + txVersion [4]uint8, + txInputVector []uint8, + txOutputVector []uint8, + txLocktime [4]uint8, + merkleProof []uint8, + txIndexInBlock *big.Int, + bitcoinHeaders []uint8, +) error { + deposit, err := tec.getDepositContract(depositAddress) + if err != nil { + return err + } + + transaction, err := deposit.ProvideRedemptionProof( + txVersion, + txInputVector, + txOutputVector, + txLocktime, + merkleProof, + txIndexInBlock, + bitcoinHeaders, + ) + if err != nil { + return err + } + + logger.Debugf( + "submitted ProvideRedemptionProof transaction with hash: [%x]", + transaction.Hash(), + ) + + return nil +} + func (tec *TBTCEthereumChain) getDepositContract( address string, ) (*contract.Deposit, error) { diff --git a/pkg/chain/event.go b/pkg/chain/event.go index adf7e3f93..abc7ad675 100644 --- a/pkg/chain/event.go +++ b/pkg/chain/event.go @@ -42,6 +42,15 @@ type KeepTerminatedEvent struct { BlockNumber uint64 } +// SignatureSubmittedEvent is an event emitted when a keep submits a signature. +type SignatureSubmittedEvent struct { + Digest [32]byte + R [32]byte + S [32]byte + RecoveryID uint8 + BlockNumber uint64 +} + // IsMember checks if list of members contains the given address. func (e *BondedECDSAKeepCreatedEvent) IsMember(address common.Address) bool { for _, member := range e.Members { diff --git a/pkg/chain/gen/eventlog/BondedECDSAKeepEventLog.go b/pkg/chain/gen/eventlog/BondedECDSAKeepEventLog.go new file mode 100644 index 000000000..1c34454d3 --- /dev/null +++ b/pkg/chain/gen/eventlog/BondedECDSAKeepEventLog.go @@ -0,0 +1,71 @@ +package eventlog + +import ( + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/keep-network/keep-ecdsa/pkg/chain/gen/abi" +) + +// FIXME: This is a temporary structure allowing to access past events +// emitted by the `BondedECDSAKeep` contract. This structure is +// here because the generated contract wrappers from `gen/contract` +// don't support `Filter*` methods yet. When the contract generator +// will support those methods, the below structure can be removed. +type BondedECDSAKeepEventLog struct { + contract *abi.BondedECDSAKeep +} + +func NewBondedECDSAKeepEventLog( + contractAddress common.Address, + backend bind.ContractBackend, +) (*BondedECDSAKeepEventLog, error) { + contract, err := abi.NewBondedECDSAKeep(contractAddress, backend) + if err != nil { + return nil, err + } + + return &BondedECDSAKeepEventLog{contract}, nil +} + +type BondedECDSAKeepSignatureSubmitted struct { + Digest [32]byte + R [32]byte + S [32]byte + RecoveryID uint8 + BlockNumber uint64 +} + +func (bekel *BondedECDSAKeepEventLog) PastSignatureSubmittedEvents( + startBlock uint64, + endBlock *uint64, +) ([]*BondedECDSAKeepSignatureSubmitted, error) { + iterator, err := bekel.contract.FilterSignatureSubmitted( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + nil, + ) + if err != nil { + return nil, err + } + + events := make([]*BondedECDSAKeepSignatureSubmitted, 0) + + for { + if !iterator.Next() { + break + } + + event := iterator.Event + events = append(events, &BondedECDSAKeepSignatureSubmitted{ + Digest: event.Digest, + R: event.R, + S: event.S, + RecoveryID: event.RecoveryID, + BlockNumber: event.Raw.BlockNumber, + }) + } + + return events, nil +} diff --git a/pkg/chain/local/bonded_ecdsa_keep.go b/pkg/chain/local/bonded_ecdsa_keep.go index 73c9e94cc..e20a1b68e 100644 --- a/pkg/chain/local/bonded_ecdsa_keep.go +++ b/pkg/chain/local/bonded_ecdsa_keep.go @@ -16,19 +16,22 @@ const ( ) type localKeep struct { - publicKey [64]byte - members []common.Address - status keepStatus + publicKey [64]byte + members []common.Address + status keepStatus + latestDigest [32]byte signatureRequestedHandlers map[int]func(event *eth.SignatureRequestedEvent) keepClosedHandlers map[int]func(event *eth.KeepClosedEvent) keepTerminatedHandlers map[int]func(event *eth.KeepTerminatedEvent) + + signatureSubmittedEvents []*eth.SignatureSubmittedEvent } func (c *localChain) requestSignature(keepAddress common.Address, digest [32]byte) error { - c.handlerMutex.Lock() - defer c.handlerMutex.Unlock() + c.localChainMutex.Lock() + defer c.localChainMutex.Unlock() keep, ok := c.keeps[keepAddress] if !ok { @@ -38,6 +41,16 @@ func (c *localChain) requestSignature(keepAddress common.Address, digest [32]byt ) } + // force the right workflow sequence + if keep.publicKey == [64]byte{} { + return fmt.Errorf( + "public key for keep [%s] is not set", + keepAddress.String(), + ) + } + + keep.latestDigest = digest + signatureRequestedEvent := ð.SignatureRequestedEvent{ Digest: digest, } @@ -52,8 +65,8 @@ func (c *localChain) requestSignature(keepAddress common.Address, digest [32]byt } func (c *localChain) closeKeep(keepAddress common.Address) error { - c.handlerMutex.Lock() - defer c.handlerMutex.Unlock() + c.localChainMutex.Lock() + defer c.localChainMutex.Unlock() keep, ok := c.keeps[keepAddress] if !ok { @@ -84,8 +97,8 @@ func (c *localChain) closeKeep(keepAddress common.Address) error { } func (c *localChain) terminateKeep(keepAddress common.Address) error { - c.handlerMutex.Lock() - defer c.handlerMutex.Unlock() + c.localChainMutex.Lock() + defer c.localChainMutex.Unlock() keep, ok := c.keeps[keepAddress] if !ok { diff --git a/pkg/chain/local/bonded_ecdsa_keep_factory.go b/pkg/chain/local/bonded_ecdsa_keep_factory.go index e0c756069..b3eea1862 100644 --- a/pkg/chain/local/bonded_ecdsa_keep_factory.go +++ b/pkg/chain/local/bonded_ecdsa_keep_factory.go @@ -15,8 +15,8 @@ func (c *localChain) createKeepWithMembers( keepAddress common.Address, members []common.Address, ) error { - c.handlerMutex.Lock() - defer c.handlerMutex.Unlock() + c.localChainMutex.Lock() + defer c.localChainMutex.Unlock() if _, ok := c.keeps[keepAddress]; ok { return fmt.Errorf( @@ -31,6 +31,7 @@ func (c *localChain) createKeepWithMembers( signatureRequestedHandlers: make(map[int]func(event *chain.SignatureRequestedEvent)), keepClosedHandlers: make(map[int]func(event *chain.KeepClosedEvent)), keepTerminatedHandlers: make(map[int]func(event *chain.KeepTerminatedEvent)), + signatureSubmittedEvents: make([]*chain.SignatureSubmittedEvent, 0), } c.keeps[keepAddress] = localKeep diff --git a/pkg/chain/local/bonded_ecdsa_keep_factory_test.go b/pkg/chain/local/bonded_ecdsa_keep_factory_test.go index 1e79452de..def995c3b 100644 --- a/pkg/chain/local/bonded_ecdsa_keep_factory_test.go +++ b/pkg/chain/local/bonded_ecdsa_keep_factory_test.go @@ -2,6 +2,7 @@ package local import ( "bytes" + "context" "fmt" "reflect" "testing" @@ -10,7 +11,10 @@ import ( ) func TestCreateKeepDuplicate(t *testing.T) { - chain := initializeLocalChain() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := initializeLocalChain(ctx) keepAddress := common.Address([20]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) expectedError := fmt.Errorf("keep already exists for address [0x0000000000000000000000000000000000000001]") @@ -30,7 +34,10 @@ func TestCreateKeepDuplicate(t *testing.T) { } func TestCreateKeep(t *testing.T) { - chain := initializeLocalChain() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := initializeLocalChain(ctx) keepAddress := common.Address([20]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) expectedPublicKey := [64]byte{} diff --git a/pkg/chain/local/bonded_ecdsa_keep_test.go b/pkg/chain/local/bonded_ecdsa_keep_test.go index 40ec15b96..30dd51c8f 100644 --- a/pkg/chain/local/bonded_ecdsa_keep_test.go +++ b/pkg/chain/local/bonded_ecdsa_keep_test.go @@ -4,16 +4,20 @@ import ( "bytes" "context" "fmt" + "math/rand" "reflect" "testing" "time" "github.com/ethereum/go-ethereum/common" - "github.com/keep-network/keep-ecdsa/pkg/chain" + eth "github.com/keep-network/keep-ecdsa/pkg/chain" ) func TestRequestSignatureNonexistentKeep(t *testing.T) { - chain := initializeLocalChain() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := initializeLocalChain(ctx) keepAddress := common.Address([20]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) digest := [32]byte{1} expectedError := fmt.Errorf("failed to find keep with address: [0x0000000000000000000000000000000000000001]") @@ -30,7 +34,10 @@ func TestRequestSignatureNonexistentKeep(t *testing.T) { } func TestRequestSignatureNoHandler(t *testing.T) { - chain := initializeLocalChain() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := initializeLocalChain(ctx) keepAddress := common.Address([20]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) digest := [32]byte{1} @@ -39,6 +46,14 @@ func TestRequestSignatureNoHandler(t *testing.T) { t.Fatal(err) } + var keepPubkey [64]byte + rand.Read(keepPubkey[:]) + + err = chain.SubmitKeepPublicKey(keepAddress, keepPubkey) + if err != nil { + t.Fatal(err) + } + err = chain.requestSignature(keepAddress, digest) if err != nil { t.Fatal(err) @@ -46,10 +61,10 @@ func TestRequestSignatureNoHandler(t *testing.T) { } func TestRequestSignature(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() + ctx, cancelCtx := context.WithTimeout(context.Background(), 1*time.Second) + defer cancelCtx() - chain := initializeLocalChain() + chain := initializeLocalChain(ctx) keepAddress := common.Address([20]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) digest := [32]byte{1} eventEmitted := make(chan *eth.SignatureRequestedEvent) @@ -61,6 +76,15 @@ func TestRequestSignature(t *testing.T) { if err != nil { t.Fatal(err) } + + var keepPubkey [64]byte + rand.Read(keepPubkey[:]) + + err = chain.SubmitKeepPublicKey(keepAddress, keepPubkey) + if err != nil { + t.Fatal(err) + } + chain.keeps[keepAddress].signatureRequestedHandlers[0] = handler err = chain.requestSignature(keepAddress, digest) diff --git a/pkg/chain/local/local.go b/pkg/chain/local/local.go index 016d7e5e2..a679e4091 100644 --- a/pkg/chain/local/local.go +++ b/pkg/chain/local/local.go @@ -1,12 +1,16 @@ package local import ( + "context" "fmt" "math/big" "math/rand" "sync" "time" + "github.com/keep-network/keep-core/pkg/chain/local" + "github.com/keep-network/keep-ecdsa/pkg/utils/byteutils" + "github.com/ethereum/go-ethereum/common" "github.com/keep-network/keep-common/pkg/subscription" "github.com/keep-network/keep-core/pkg/chain" @@ -30,7 +34,10 @@ type Chain interface { // It mocks the behaviour of a real blockchain, without the complexity of deployments, // accounts, async transactions and so on. For use in tests ONLY. type localChain struct { - handlerMutex sync.Mutex + localChainMutex sync.Mutex + + blockCounter chain.BlockCounter + blocksTimestamps sync.Map keepAddresses []common.Address keeps map[common.Address]*localKeep @@ -44,13 +51,39 @@ type localChain struct { // Connect performs initialization for communication with Ethereum blockchain // based on provided config. -func Connect() Chain { - return &localChain{ +func Connect(ctx context.Context) Chain { + blockCounter, err := local.BlockCounter() + if err != nil { + panic(err) // should never happen + } + + localChain := &localChain{ + blockCounter: blockCounter, keeps: make(map[common.Address]*localKeep), keepCreatedHandlers: make(map[int]func(event *eth.BondedECDSAKeepCreatedEvent)), clientAddress: common.HexToAddress("6299496199d99941193Fdd2d717ef585F431eA05"), authorizations: make(map[common.Address]bool), } + + // block 0 must be stored manually as it is not delivered by the block counter + localChain.blocksTimestamps.Store(uint64(0), uint64(time.Now().Unix())) + + go localChain.observeBlocksTimestamps(ctx) + + return localChain +} + +func (lc *localChain) observeBlocksTimestamps(ctx context.Context) { + blockChan := lc.BlockCounter().WatchBlocks(ctx) + + for { + select { + case blockNumber := <-blockChan: + lc.blocksTimestamps.Store(blockNumber, uint64(time.Now().Unix())) + case <-ctx.Done(): + return + } + } } func (lc *localChain) OpenKeep(keepAddress common.Address, members []common.Address) { @@ -69,8 +102,8 @@ func (lc *localChain) TerminateKeep(keepAddress common.Address) error { } func (lc *localChain) AuthorizeOperator(operator common.Address) { - lc.handlerMutex.Lock() - defer lc.handlerMutex.Unlock() + lc.localChainMutex.Lock() + defer lc.localChainMutex.Unlock() lc.authorizations[operator] = true } @@ -95,16 +128,16 @@ func (lc *localChain) RegisterAsMemberCandidate(application common.Address) erro func (lc *localChain) OnBondedECDSAKeepCreated( handler func(event *eth.BondedECDSAKeepCreatedEvent), ) subscription.EventSubscription { - lc.handlerMutex.Lock() - defer lc.handlerMutex.Unlock() + lc.localChainMutex.Lock() + defer lc.localChainMutex.Unlock() handlerID := generateHandlerID() lc.keepCreatedHandlers[handlerID] = handler return subscription.NewEventSubscription(func() { - lc.handlerMutex.Lock() - defer lc.handlerMutex.Unlock() + lc.localChainMutex.Lock() + defer lc.localChainMutex.Unlock() delete(lc.keepCreatedHandlers, handlerID) }) @@ -116,8 +149,8 @@ func (lc *localChain) OnSignatureRequested( keepAddress common.Address, handler func(event *eth.SignatureRequestedEvent), ) (subscription.EventSubscription, error) { - lc.handlerMutex.Lock() - defer lc.handlerMutex.Unlock() + lc.localChainMutex.Lock() + defer lc.localChainMutex.Unlock() handlerID := generateHandlerID() @@ -132,8 +165,8 @@ func (lc *localChain) OnSignatureRequested( keep.signatureRequestedHandlers[handlerID] = handler return subscription.NewEventSubscription(func() { - lc.handlerMutex.Lock() - defer lc.handlerMutex.Unlock() + lc.localChainMutex.Lock() + defer lc.localChainMutex.Unlock() delete(keep.signatureRequestedHandlers, handlerID) }), nil @@ -145,6 +178,9 @@ func (lc *localChain) SubmitKeepPublicKey( keepAddress common.Address, publicKey [64]byte, ) error { + lc.localChainMutex.Lock() + defer lc.localChainMutex.Unlock() + keep, ok := lc.keeps[keepAddress] if !ok { return fmt.Errorf( @@ -171,6 +207,45 @@ func (lc *localChain) SubmitSignature( keepAddress common.Address, signature *ecdsa.Signature, ) error { + lc.localChainMutex.Lock() + defer lc.localChainMutex.Unlock() + + keep, ok := lc.keeps[keepAddress] + if !ok { + return fmt.Errorf( + "failed to find keep with address: [%s]", + keepAddress.String(), + ) + } + + // force the right workflow sequence + if keep.latestDigest == [32]byte{} { + return fmt.Errorf( + "keep [%s] is not awaiting for a signature", + keepAddress.String(), + ) + } + + rBytes, err := byteutils.BytesTo32Byte(signature.R.Bytes()) + if err != nil { + return err + } + + sBytes, err := byteutils.BytesTo32Byte(signature.S.Bytes()) + if err != nil { + return err + } + + keep.signatureSubmittedEvents = append( + keep.signatureSubmittedEvents, + ð.SignatureSubmittedEvent{ + Digest: keep.latestDigest, + R: rBytes, + S: sBytes, + RecoveryID: uint8(signature.RecoveryID), + }, + ) + return nil } @@ -185,8 +260,8 @@ func (lc *localChain) IsAwaitingSignature( // IsActive checks for current state of a keep on-chain. func (lc *localChain) IsActive(keepAddress common.Address) (bool, error) { - lc.handlerMutex.Lock() - defer lc.handlerMutex.Unlock() + lc.localChainMutex.Lock() + defer lc.localChainMutex.Unlock() keep, ok := lc.keeps[keepAddress] if !ok { @@ -197,7 +272,7 @@ func (lc *localChain) IsActive(keepAddress common.Address) (bool, error) { } func (lc *localChain) BlockCounter() chain.BlockCounter { - panic("implement") + return lc.blockCounter } func (lc *localChain) IsRegisteredForApplication(application common.Address) (bool, error) { @@ -217,15 +292,15 @@ func (lc *localChain) UpdateStatusForApplication(application common.Address) err } func (lc *localChain) IsOperatorAuthorized(operator common.Address) (bool, error) { - lc.handlerMutex.Lock() - defer lc.handlerMutex.Unlock() + lc.localChainMutex.Lock() + defer lc.localChainMutex.Unlock() return lc.authorizations[operator], nil } func (lc *localChain) GetKeepCount() (*big.Int, error) { - lc.handlerMutex.Lock() - defer lc.handlerMutex.Unlock() + lc.localChainMutex.Lock() + defer lc.localChainMutex.Unlock() return big.NewInt(int64(len(lc.keeps))), nil } @@ -233,8 +308,8 @@ func (lc *localChain) GetKeepCount() (*big.Int, error) { func (lc *localChain) GetKeepAtIndex( keepIndex *big.Int, ) (common.Address, error) { - lc.handlerMutex.Lock() - defer lc.handlerMutex.Unlock() + lc.localChainMutex.Lock() + defer lc.localChainMutex.Unlock() index := int(keepIndex.Uint64()) @@ -249,8 +324,8 @@ func (lc *localChain) OnKeepClosed( keepAddress common.Address, handler func(event *eth.KeepClosedEvent), ) (subscription.EventSubscription, error) { - lc.handlerMutex.Lock() - defer lc.handlerMutex.Unlock() + lc.localChainMutex.Lock() + defer lc.localChainMutex.Unlock() handlerID := generateHandlerID() @@ -265,8 +340,8 @@ func (lc *localChain) OnKeepClosed( keep.keepClosedHandlers[handlerID] = handler return subscription.NewEventSubscription(func() { - lc.handlerMutex.Lock() - defer lc.handlerMutex.Unlock() + lc.localChainMutex.Lock() + defer lc.localChainMutex.Unlock() delete(keep.keepClosedHandlers, handlerID) }), nil @@ -276,8 +351,8 @@ func (lc *localChain) OnKeepTerminated( keepAddress common.Address, handler func(event *eth.KeepTerminatedEvent), ) (subscription.EventSubscription, error) { - lc.handlerMutex.Lock() - defer lc.handlerMutex.Unlock() + lc.localChainMutex.Lock() + defer lc.localChainMutex.Unlock() handlerID := generateHandlerID() @@ -292,8 +367,8 @@ func (lc *localChain) OnKeepTerminated( keep.keepTerminatedHandlers[handlerID] = handler return subscription.NewEventSubscription(func() { - lc.handlerMutex.Lock() - defer lc.handlerMutex.Unlock() + lc.localChainMutex.Lock() + defer lc.localChainMutex.Unlock() delete(keep.keepTerminatedHandlers, handlerID) }), nil @@ -331,8 +406,8 @@ func (lc *localChain) GetPublicKey(keepAddress common.Address) ([]uint8, error) func (lc *localChain) GetMembers( keepAddress common.Address, ) ([]common.Address, error) { - lc.handlerMutex.Lock() - defer lc.handlerMutex.Unlock() + lc.localChainMutex.Lock() + defer lc.localChainMutex.Unlock() keep, ok := lc.keeps[keepAddress] if !ok { @@ -351,6 +426,30 @@ func (lc *localChain) GetOpenedTimestamp(keepAddress common.Address) (time.Time, panic("implement") } +func (lc *localChain) PastSignatureSubmittedEvents( + keepAddress string, + startBlock uint64, +) ([]*eth.SignatureSubmittedEvent, error) { + lc.localChainMutex.Lock() + defer lc.localChainMutex.Unlock() + + keep, ok := lc.keeps[common.HexToAddress(keepAddress)] + if !ok { + return nil, fmt.Errorf("no keep with address [%v]", keepAddress) + } + + return keep.signatureSubmittedEvents, nil +} + +func (lc *localChain) BlockTimestamp(blockNumber *big.Int) (uint64, error) { + blockTimestamp, ok := lc.blocksTimestamps.Load(blockNumber.Uint64()) + if !ok { + return 0, fmt.Errorf("no timestamp for block [%v]", blockNumber) + } + + return blockTimestamp.(uint64), nil +} + func generateHandlerID() int { // #nosec G404 (insecure random number source (rand)) // Local chain implementation doesn't require secure randomness. diff --git a/pkg/chain/local/local_test.go b/pkg/chain/local/local_test.go index f6e1a36a5..0728b6a3a 100644 --- a/pkg/chain/local/local_test.go +++ b/pkg/chain/local/local_test.go @@ -3,19 +3,24 @@ package local import ( "context" "fmt" + "math/big" + "math/rand" "reflect" "testing" "time" + "github.com/keep-network/keep-ecdsa/pkg/ecdsa" + "github.com/keep-network/keep-ecdsa/pkg/utils/byteutils" + "github.com/ethereum/go-ethereum/common" - "github.com/keep-network/keep-ecdsa/pkg/chain" + eth "github.com/keep-network/keep-ecdsa/pkg/chain" ) func TestOnBondedECDSAKeepCreated(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() + ctx, cancelCtx := context.WithTimeout(context.Background(), 1*time.Second) + defer cancelCtx() - chain := initializeLocalChain() + chain := initializeLocalChain(ctx) eventFired := make(chan *eth.BondedECDSAKeepCreatedEvent) keepAddress := common.Address([20]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) expectedEvent := ð.BondedECDSAKeepCreatedEvent{ @@ -49,10 +54,10 @@ func TestOnBondedECDSAKeepCreated(t *testing.T) { } func TestOnSignatureRequested(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() + ctx, cancelCtx := context.WithTimeout(context.Background(), 1*time.Second) + defer cancelCtx() - chain := initializeLocalChain() + chain := initializeLocalChain(ctx) eventFired := make(chan *eth.SignatureRequestedEvent) keepAddress := common.Address([20]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) digest := [32]byte{1} @@ -62,6 +67,14 @@ func TestOnSignatureRequested(t *testing.T) { t.Fatal(err) } + var keepPubkey [64]byte + rand.Read(keepPubkey[:]) + + err = chain.SubmitKeepPublicKey(keepAddress, keepPubkey) + if err != nil { + t.Fatal(err) + } + subscription, err := chain.OnSignatureRequested( keepAddress, func(event *eth.SignatureRequestedEvent) { @@ -97,7 +110,10 @@ func TestOnSignatureRequested(t *testing.T) { } func TestSubmitKeepPublicKey(t *testing.T) { - chain := initializeLocalChain() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := initializeLocalChain(ctx) keepAddress := common.HexToAddress("0x41048F9B90290A2e96D07f537F3A7E97620E9e47") keepPublicKey := [64]byte{11, 12, 13, 14, 15, 16} expectedDuplicationError := fmt.Errorf( @@ -139,6 +155,76 @@ func TestSubmitKeepPublicKey(t *testing.T) { } } -func initializeLocalChain() *localChain { - return Connect().(*localChain) +func TestSubmitSignature(t *testing.T) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := initializeLocalChain(ctx) + + keepAddress := common.HexToAddress("0x41048F9B90290A2e96D07f537F3A7E97620E9e47") + keepPublicKey := [64]byte{11, 12, 13, 14, 15, 16} + + err := chain.createKeep(keepAddress) + if err != nil { + t.Fatal(err) + } + + err = chain.SubmitKeepPublicKey( + keepAddress, + keepPublicKey, + ) + if err != nil { + t.Fatal(err) + } + + digest := [32]byte{17, 18} + + err = chain.requestSignature(keepAddress, digest) + if err != nil { + t.Fatal(err) + } + + signature := &ecdsa.Signature{ + R: big.NewInt(10), + S: big.NewInt(11), + RecoveryID: 1, + } + + err = chain.SubmitSignature(keepAddress, signature) + if err != nil { + t.Fatal(err) + } + + events, err := chain.PastSignatureSubmittedEvents(keepAddress.Hex(), 0) + if err != nil { + t.Fatal(err) + } + + if len(events) != 1 { + t.Errorf("there should be one signature submitted event") + } + + expectedRBytes, _ := byteutils.BytesTo32Byte(signature.R.Bytes()) + expectedSBytes, _ := byteutils.BytesTo32Byte(signature.S.Bytes()) + expectedEvent := ð.SignatureSubmittedEvent{ + Digest: digest, + R: expectedRBytes, + S: expectedSBytes, + RecoveryID: 1, + BlockNumber: 0, + } + + lastEvent := events[len(events)-1] + + if !reflect.DeepEqual(expectedEvent, lastEvent) { + t.Fatalf( + "unexpected signature submitted event\nexpected: [%+v]\nactual: [%+v]", + expectedEvent, + lastEvent, + ) + } +} + +func initializeLocalChain(ctx context.Context) *localChain { + return Connect(ctx).(*localChain) } diff --git a/pkg/chain/local/tbtc.go b/pkg/chain/local/tbtc.go index c3e6d2198..c0d951bff 100644 --- a/pkg/chain/local/tbtc.go +++ b/pkg/chain/local/tbtc.go @@ -2,20 +2,49 @@ package local import ( "bytes" + "context" + "encoding/binary" "fmt" + "math/big" + "math/rand" "sync" + chain "github.com/keep-network/keep-ecdsa/pkg/chain" + "github.com/ethereum/go-ethereum/common" "github.com/keep-network/keep-common/pkg/subscription" ) +const ( + defaultUTXOValue = 1000 + defaultInitialRedemptionFee = 10 +) + type localDeposit struct { keepAddress string pubkey []byte + + utxoValue *big.Int + redemptionDigest [32]byte + redemptionFee *big.Int + redemptionSignature *Signature + redemptionProof *TxProof + + redemptionRequestedEvents []*chain.DepositRedemptionRequestedEvent +} + +type Signature struct { + V uint8 + R [32]uint8 + S [32]uint8 } +type TxProof struct{} + type localChainLogger struct { - retrieveSignerPubkeyCalls int + retrieveSignerPubkeyCalls int + provideRedemptionSignatureCalls int + increaseRedemptionFeeCalls int } func (lcl *localChainLogger) logRetrieveSignerPubkeyCall() { @@ -26,31 +55,56 @@ func (lcl *localChainLogger) RetrieveSignerPubkeyCalls() int { return lcl.retrieveSignerPubkeyCalls } +func (lcl *localChainLogger) logProvideRedemptionSignatureCall() { + lcl.provideRedemptionSignatureCalls++ +} + +func (lcl *localChainLogger) ProvideRedemptionSignatureCalls() int { + return lcl.provideRedemptionSignatureCalls +} + +func (lcl *localChainLogger) logIncreaseRedemptionFeeCalls() { + lcl.increaseRedemptionFeeCalls++ +} + +func (lcl *localChainLogger) IncreaseRedemptionFeeCalls() int { + return lcl.increaseRedemptionFeeCalls +} + type TBTCLocalChain struct { *localChain - mutex sync.Mutex + tbtcLocalChainMutex sync.Mutex logger *localChainLogger - deposits map[string]*localDeposit - depositCreatedHandlers map[int]func(depositAddress string) - depositRegisteredPubkeyHandlers map[int]func(depositAddress string) + alwaysFailingTransactions map[string]bool + + deposits map[string]*localDeposit + depositCreatedHandlers map[int]func(depositAddress string) + depositRegisteredPubkeyHandlers map[int]func(depositAddress string) + depositRedemptionRequestedHandlers map[int]func(depositAddress string) + depositGotRedemptionSignatureHandlers map[int]func(depositAddress string) + depositRedeemedHandlers map[int]func(depositAddress string) } -func NewTBTCLocalChain() *TBTCLocalChain { +func NewTBTCLocalChain(ctx context.Context) *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)), + localChain: Connect(ctx).(*localChain), + logger: &localChainLogger{}, + alwaysFailingTransactions: make(map[string]bool), + deposits: make(map[string]*localDeposit), + depositCreatedHandlers: make(map[int]func(depositAddress string)), + depositRegisteredPubkeyHandlers: make(map[int]func(depositAddress string)), + depositRedemptionRequestedHandlers: make(map[int]func(depositAddress string)), + depositGotRedemptionSignatureHandlers: make(map[int]func(depositAddress string)), + depositRedeemedHandlers: make(map[int]func(depositAddress string)), } } func (tlc *TBTCLocalChain) CreateDeposit(depositAddress string) { - tlc.mutex.Lock() - defer tlc.mutex.Unlock() + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() keepAddress := generateAddress() tlc.OpenKeep(keepAddress, []common.Address{ @@ -60,7 +114,9 @@ func (tlc *TBTCLocalChain) CreateDeposit(depositAddress string) { }) tlc.deposits[depositAddress] = &localDeposit{ - keepAddress: keepAddress.Hex(), + keepAddress: keepAddress.Hex(), + utxoValue: big.NewInt(defaultUTXOValue), + redemptionRequestedEvents: make([]*chain.DepositRedemptionRequestedEvent, 0), } for _, handler := range tlc.depositCreatedHandlers { @@ -73,16 +129,16 @@ func (tlc *TBTCLocalChain) CreateDeposit(depositAddress string) { func (tlc *TBTCLocalChain) OnDepositCreated( handler func(depositAddress string), ) (subscription.EventSubscription, error) { - tlc.mutex.Lock() - defer tlc.mutex.Unlock() + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() handlerID := generateHandlerID() tlc.depositCreatedHandlers[handlerID] = handler return subscription.NewEventSubscription(func() { - tlc.mutex.Lock() - defer tlc.mutex.Unlock() + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() delete(tlc.depositCreatedHandlers, handlerID) }), nil @@ -91,24 +147,158 @@ func (tlc *TBTCLocalChain) OnDepositCreated( func (tlc *TBTCLocalChain) OnDepositRegisteredPubkey( handler func(depositAddress string), ) (subscription.EventSubscription, error) { - tlc.mutex.Lock() - defer tlc.mutex.Unlock() + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() handlerID := generateHandlerID() tlc.depositRegisteredPubkeyHandlers[handlerID] = handler return subscription.NewEventSubscription(func() { - tlc.mutex.Lock() - defer tlc.mutex.Unlock() + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() delete(tlc.depositRegisteredPubkeyHandlers, handlerID) }), nil } +func (tlc *TBTCLocalChain) RedeemDeposit(depositAddress string) error { + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() + + deposit, ok := tlc.deposits[depositAddress] + if !ok { + return fmt.Errorf("no deposit with address [%v]", depositAddress) + } + + if !bytes.Equal( + deposit.redemptionDigest[:], + make([]byte, len(deposit.redemptionDigest)), + ) { + return fmt.Errorf( + "redemption of deposit [%v] already requested", + depositAddress, + ) + } + + var randomDigest [32]byte + // #nosec G404 (insecure random number source (rand)) + // Local chain implementation doesn't require secure randomness. + _, err := rand.Read(randomDigest[:]) + if err != nil { + return err + } + + deposit.redemptionDigest = randomDigest + deposit.redemptionFee = big.NewInt(defaultInitialRedemptionFee) + + err = tlc.requestSignature( + common.HexToAddress(deposit.keepAddress), + deposit.redemptionDigest, + ) + if err != nil { + return err + } + + for _, handler := range tlc.depositRedemptionRequestedHandlers { + go func(handler func(depositAddress string), depositAddress string) { + handler(depositAddress) + }(handler, depositAddress) + } + + currentBlock, err := tlc.BlockCounter().CurrentBlock() + if err != nil { + return err + } + + deposit.redemptionRequestedEvents = append( + deposit.redemptionRequestedEvents, + &chain.DepositRedemptionRequestedEvent{ + DepositAddress: depositAddress, + Digest: deposit.redemptionDigest, + UtxoValue: deposit.utxoValue, + RedeemerOutputScript: nil, + RequestedFee: deposit.redemptionFee, + Outpoint: nil, + BlockNumber: currentBlock, + }, + ) + + return nil +} + +func (tlc *TBTCLocalChain) OnDepositRedemptionRequested( + handler func(depositAddress string), +) (subscription.EventSubscription, error) { + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() + + handlerID := generateHandlerID() + + tlc.depositRedemptionRequestedHandlers[handlerID] = handler + + return subscription.NewEventSubscription(func() { + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() + + delete(tlc.depositRedemptionRequestedHandlers, handlerID) + }), nil +} + +func (tlc *TBTCLocalChain) OnDepositGotRedemptionSignature( + handler func(depositAddress string), +) (subscription.EventSubscription, error) { + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() + + handlerID := generateHandlerID() + + tlc.depositGotRedemptionSignatureHandlers[handlerID] = handler + + return subscription.NewEventSubscription(func() { + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() + + delete(tlc.depositGotRedemptionSignatureHandlers, handlerID) + }), nil +} + +func (tlc *TBTCLocalChain) OnDepositRedeemed( + handler func(depositAddress string), +) (subscription.EventSubscription, error) { + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() + + handlerID := generateHandlerID() + + tlc.depositRedeemedHandlers[handlerID] = handler + + return subscription.NewEventSubscription(func() { + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() + + delete(tlc.depositRedeemedHandlers, handlerID) + }), nil +} + +func (tlc *TBTCLocalChain) PastDepositRedemptionRequestedEvents( + depositAddress string, + startBlock uint64, +) ([]*chain.DepositRedemptionRequestedEvent, error) { + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() + + deposit, ok := tlc.deposits[depositAddress] + if !ok { + return nil, fmt.Errorf("no deposit with address [%v]", depositAddress) + } + + return deposit.redemptionRequestedEvents, nil +} + func (tlc *TBTCLocalChain) KeepAddress(depositAddress string) (string, error) { - tlc.mutex.Lock() - defer tlc.mutex.Unlock() + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() deposit, ok := tlc.deposits[depositAddress] if !ok { @@ -119,8 +309,8 @@ func (tlc *TBTCLocalChain) KeepAddress(depositAddress string) (string, error) { } func (tlc *TBTCLocalChain) RetrieveSignerPubkey(depositAddress string) error { - tlc.mutex.Lock() - defer tlc.mutex.Unlock() + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() tlc.logger.logRetrieveSignerPubkeyCall() @@ -137,8 +327,8 @@ func (tlc *TBTCLocalChain) RetrieveSignerPubkey(depositAddress string) error { } // lock upstream mutex to access `keeps` map safely - tlc.handlerMutex.Lock() - defer tlc.handlerMutex.Unlock() + tlc.localChainMutex.Lock() + defer tlc.localChainMutex.Unlock() keep, ok := tlc.keeps[common.HexToAddress(deposit.keepAddress)] if !ok { @@ -167,11 +357,184 @@ func (tlc *TBTCLocalChain) RetrieveSignerPubkey(depositAddress string) error { return nil } +func (tlc *TBTCLocalChain) ProvideRedemptionSignature( + depositAddress string, + v uint8, + r [32]uint8, + s [32]uint8, +) error { + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() + + tlc.logger.logProvideRedemptionSignatureCall() + + if _, exists := tlc.alwaysFailingTransactions["ProvideRedemptionSignature"]; exists { + return fmt.Errorf("always failing transaction") + } + + deposit, ok := tlc.deposits[depositAddress] + if !ok { + return fmt.Errorf("no deposit with address [%v]", depositAddress) + } + + if deposit.redemptionDigest == [32]byte{} { + return fmt.Errorf("deposit [%v] is not in redemption", depositAddress) + } + + if deposit.redemptionSignature != nil { + return fmt.Errorf( + "redemption signature for deposit [%v] already provided", + depositAddress, + ) + } + + deposit.redemptionSignature = &Signature{ + V: v, + R: r, + S: s, + } + + for _, handler := range tlc.depositGotRedemptionSignatureHandlers { + go func(handler func(depositAddress string), depositAddress string) { + handler(depositAddress) + }(handler, depositAddress) + } + + return nil +} + +func (tlc *TBTCLocalChain) IncreaseRedemptionFee( + depositAddress string, + previousOutputValueBytes [8]uint8, + newOutputValueBytes [8]uint8, +) error { + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() + + tlc.logger.logIncreaseRedemptionFeeCalls() + + if _, exists := tlc.alwaysFailingTransactions["IncreaseRedemptionFee"]; exists { + return fmt.Errorf("always failing transaction") + } + + deposit, ok := tlc.deposits[depositAddress] + if !ok { + return fmt.Errorf("no deposit with address [%v]", depositAddress) + } + + if deposit.redemptionSignature == nil { + return fmt.Errorf( + "no redemption signature for deposit [%v]; could not increase fee", + depositAddress, + ) + } + + previousOutputValue := fromLittleEndianBytes(previousOutputValueBytes) + expectedPreviousOutputValue := new(big.Int).Sub( + deposit.utxoValue, + deposit.redemptionFee, + ) + + if expectedPreviousOutputValue.Cmp(previousOutputValue) != 0 { + return fmt.Errorf("wrong previous output value") + } + + newOutputValue := fromLittleEndianBytes(newOutputValueBytes) + + if new(big.Int).Sub(previousOutputValue, newOutputValue).Cmp( + big.NewInt(defaultInitialRedemptionFee), + ) != 0 { + return fmt.Errorf("wrong increase fee step") + } + + var randomDigest [32]byte + // #nosec G404 (insecure random number source (rand)) + // Local chain implementation doesn't require secure randomness. + _, err := rand.Read(randomDigest[:]) + if err != nil { + return err + } + + deposit.redemptionDigest = randomDigest + deposit.redemptionFee = new(big.Int).Sub(deposit.utxoValue, newOutputValue) + deposit.redemptionSignature = nil + + err = tlc.requestSignature( + common.HexToAddress(deposit.keepAddress), + deposit.redemptionDigest, + ) + if err != nil { + return err + } + + for _, handler := range tlc.depositRedemptionRequestedHandlers { + go func(handler func(depositAddress string), depositAddress string) { + handler(depositAddress) + }(handler, depositAddress) + } + + currentBlock, err := tlc.BlockCounter().CurrentBlock() + if err != nil { + return err + } + + deposit.redemptionRequestedEvents = append( + deposit.redemptionRequestedEvents, + &chain.DepositRedemptionRequestedEvent{ + DepositAddress: depositAddress, + Digest: deposit.redemptionDigest, + UtxoValue: deposit.utxoValue, + RedeemerOutputScript: nil, + RequestedFee: deposit.redemptionFee, + Outpoint: nil, + BlockNumber: currentBlock, + }, + ) + + return nil +} + +func (tlc *TBTCLocalChain) ProvideRedemptionProof( + depositAddress string, + txVersion [4]uint8, + txInputVector []uint8, + txOutputVector []uint8, + txLocktime [4]uint8, + merkleProof []uint8, + txIndexInBlock *big.Int, + bitcoinHeaders []uint8, +) error { + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() + + deposit, ok := tlc.deposits[depositAddress] + if !ok { + return fmt.Errorf("no deposit with address [%v]", depositAddress) + } + + if deposit.redemptionProof != nil { + return fmt.Errorf( + "redemption proof for deposit [%v] already provided", + depositAddress, + ) + } + + deposit.redemptionProof = &TxProof{} + + for _, handler := range tlc.depositRedeemedHandlers { + 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() + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() deposit, ok := tlc.deposits[depositAddress] if !ok { @@ -188,6 +551,82 @@ func (tlc *TBTCLocalChain) DepositPubkey( return deposit.pubkey, nil } +func (tlc *TBTCLocalChain) DepositRedemptionSignature( + depositAddress string, +) (*Signature, error) { + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() + + deposit, ok := tlc.deposits[depositAddress] + if !ok { + return nil, fmt.Errorf("no deposit with address [%v]", depositAddress) + } + + if deposit.redemptionSignature == nil { + return nil, fmt.Errorf( + "no redemption signature for deposit [%v]", + depositAddress, + ) + } + + return deposit.redemptionSignature, nil +} + +func (tlc *TBTCLocalChain) DepositRedemptionProof( + depositAddress string, +) (*TxProof, error) { + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() + + deposit, ok := tlc.deposits[depositAddress] + if !ok { + return nil, fmt.Errorf("no deposit with address [%v]", depositAddress) + } + + if deposit.redemptionProof == nil { + return nil, fmt.Errorf( + "no redemption proof for deposit [%v]", + depositAddress, + ) + } + + return deposit.redemptionProof, nil +} + +func (tlc *TBTCLocalChain) DepositRedemptionFee( + depositAddress string, +) (*big.Int, error) { + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() + + deposit, ok := tlc.deposits[depositAddress] + if !ok { + return nil, fmt.Errorf("no deposit with address [%v]", depositAddress) + } + + if deposit.redemptionFee == nil { + return nil, fmt.Errorf( + "no redemption fee for deposit [%v]", + depositAddress, + ) + } + + return deposit.redemptionFee, nil +} + +func (tlc *TBTCLocalChain) SetAlwaysFailingTransactions(transactions ...string) { + tlc.tbtcLocalChainMutex.Lock() + defer tlc.tbtcLocalChainMutex.Unlock() + + for _, tx := range transactions { + tlc.alwaysFailingTransactions[tx] = true + } +} + func (tlc *TBTCLocalChain) Logger() *localChainLogger { return tlc.logger } + +func fromLittleEndianBytes(bytes [8]byte) *big.Int { + return new(big.Int).SetUint64(binary.LittleEndian.Uint64(bytes[:])) +} diff --git a/pkg/chain/tbtc.go b/pkg/chain/tbtc.go new file mode 100644 index 000000000..ee248a1c7 --- /dev/null +++ b/pkg/chain/tbtc.go @@ -0,0 +1,114 @@ +package eth + +import ( + "math/big" + + "github.com/keep-network/keep-common/pkg/subscription" +) + +// TBTCHandle represents a chain handle extended with TBTC-specific capabilities. +type TBTCHandle interface { + 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 + + // ProvideRedemptionSignature provides the redemption signature for the + // provided deposit. + ProvideRedemptionSignature( + depositAddress string, + v uint8, + r [32]uint8, + s [32]uint8, + ) error + + // IncreaseRedemptionFee increases the redemption fee for the + // provided deposit. + IncreaseRedemptionFee( + depositAddress string, + previousOutputValueBytes [8]uint8, + newOutputValueBytes [8]uint8, + ) error + + // ProvideRedemptionProof provides the redemption proof for the + // provided deposit. + ProvideRedemptionProof( + depositAddress string, + txVersion [4]uint8, + txInputVector []uint8, + txOutputVector []uint8, + txLocktime [4]uint8, + merkleProof []uint8, + txIndexInBlock *big.Int, + bitcoinHeaders []uint8, + ) 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) + + // OnDepositRedemptionRequested installs a callback that is invoked when an + // on-chain notification of a deposit redemption request is seen. + OnDepositRedemptionRequested( + handler func(depositAddress string), + ) (subscription.EventSubscription, error) + + // OnDepositGotRedemptionSignature installs a callback that is invoked + // when an on-chain notification of a deposit receiving a redemption + // signature is seen. + OnDepositGotRedemptionSignature( + handler func(depositAddress string), + ) (subscription.EventSubscription, error) + + // OnDepositRedeemed installs a callback that is invoked when an + // on-chain notification of a deposit redemption is seen. + OnDepositRedeemed( + handler func(depositAddress string), + ) (subscription.EventSubscription, error) + + // PastDepositRedemptionRequestedEvents returns all redemption requested + // events for the given deposit which occurred after the provided start block. + // All implementations should returns those events sorted by the + // block number in the ascending order. + PastDepositRedemptionRequestedEvents( + depositAddress string, + startBlock uint64, + ) ([]*DepositRedemptionRequestedEvent, error) +} + +// DepositRedemptionRequestedEvent is an event emitted when a deposit +// redemption has been requested or the redemption fee has been increased. +type DepositRedemptionRequestedEvent struct { + DepositAddress string + RequesterAddress string + Digest [32]byte + UtxoValue *big.Int + RedeemerOutputScript []byte + RequestedFee *big.Int + Outpoint []byte + BlockNumber uint64 +} diff --git a/pkg/extensions/tbtc/chain.go b/pkg/extensions/tbtc/chain.go deleted file mode 100644 index 598814880..000000000 --- a/pkg/extensions/tbtc/chain.go +++ /dev/null @@ -1,42 +0,0 @@ -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 index bf88c0f06..5052617dc 100644 --- a/pkg/extensions/tbtc/tbtc.go +++ b/pkg/extensions/tbtc/tbtc.go @@ -1,9 +1,12 @@ package tbtc import ( + "bytes" "context" + "encoding/binary" "fmt" "math" + "math/big" "math/rand" "time" @@ -16,18 +19,21 @@ import ( var logger = log.Logger("tbtc-extension") -const maxActAttempts = 3 +const ( + maxActAttempts = 3 + pastEventsLookbackBlocks = 10000 +) // Initialize initializes extension specific to the TBTC application. -func Initialize(ctx context.Context, handle Handle) error { +func Initialize(ctx context.Context, chain chain.TBTCHandle) error { logger.Infof("initializing tbtc extension") - tbtc := &tbtc{handle} + tbtc := newTBTC(chain) err := tbtc.monitorRetrievePubKey( ctx, exponentialBackoff, - 150*time.Minute, + 165*time.Minute, // 15 minutes before the 3 hours on-chain timeout ) if err != nil { return fmt.Errorf( @@ -36,29 +42,45 @@ func Initialize(ctx context.Context, handle Handle) error { ) } + err = tbtc.monitorProvideRedemptionSignature( + ctx, + exponentialBackoff, + 105*time.Minute, // 15 minutes before the 2 hours on-chain timeout + ) + if err != nil { + return fmt.Errorf( + "could not initialize provide redemption "+ + "signature monitoring: [%v]", + err, + ) + } + + err = tbtc.monitorProvideRedemptionProof( + ctx, + exponentialBackoff, + 345*time.Minute, // 15 minutes before the 6 hours on-chain timeout + ) + if err != nil { + return fmt.Errorf( + "could not initialize provide redemption "+ + "proof 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 + chain chain.TBTCHandle +} + +func newTBTC(chain chain.TBTCHandle) *tbtc { + return &tbtc{ + chain: chain, + } } func (t *tbtc) monitorRetrievePubKey( @@ -76,11 +98,11 @@ func (t *tbtc) monitorRetrievePubKey( return t.chain.OnDepositRegisteredPubkey(handler) }, t.watchKeepClosed, - func(deposit string) error { - return t.chain.RetrieveSignerPubkey(deposit) - }, + t.chain.RetrieveSignerPubkey, actBackoffFn, - timeout, + func(_ string) (time.Duration, error) { + return timeout, nil + }, ) if err != nil { return err @@ -97,10 +119,301 @@ func (t *tbtc) monitorRetrievePubKey( return nil } -// TODO: +func (t *tbtc) monitorProvideRedemptionSignature( + ctx context.Context, + actBackoffFn backoffFn, + timeout time.Duration, +) error { + monitoringStartFn := func( + handler depositEventHandler, + ) (subscription.EventSubscription, error) { + // Start right after a redemption has been requested or the redemption + // fee has been increased. + return t.chain.OnDepositRedemptionRequested(handler) + } + + monitoringStopFn := func( + handler depositEventHandler, + ) (subscription.EventSubscription, error) { + // Stop in case the redemption signature has been provided by someone else. + signatureSubscription, err := t.chain.OnDepositGotRedemptionSignature(handler) + if err != nil { + return nil, err + } + + // Stop in case the redemption proof has been provided by someone else. + redeemedSubscription, err := t.chain.OnDepositRedeemed(handler) + if err != nil { + return nil, err + } + + return subscription.NewEventSubscription( + func() { + signatureSubscription.Unsubscribe() + redeemedSubscription.Unsubscribe() + }, + ), nil + } + + actFn := func(depositAddress string) error { + keepAddress, err := t.chain.KeepAddress(depositAddress) + if err != nil { + return err + } + + redemptionRequestedEvents, err := t.chain.PastDepositRedemptionRequestedEvents( + depositAddress, + t.pastEventsLookupStartBlock(), + ) + if err != nil { + return err + } + + if len(redemptionRequestedEvents) == 0 { + return fmt.Errorf( + "no redemption requested events found for deposit: [%v]", + depositAddress, + ) + } + + latestRedemptionRequestedEvent := + redemptionRequestedEvents[len(redemptionRequestedEvents)-1] + + signatureSubmittedEvents, err := t.chain.PastSignatureSubmittedEvents( + keepAddress, + latestRedemptionRequestedEvent.BlockNumber, + ) + if err != nil { + return err + } + + if len(signatureSubmittedEvents) == 0 { + return fmt.Errorf( + "no signature submitted events found for deposit: [%v]", + depositAddress, + ) + } + + latestSignatureSubmittedEvent := + signatureSubmittedEvents[len(signatureSubmittedEvents)-1] + + depositDigest := latestRedemptionRequestedEvent.Digest + + if bytes.Equal(latestSignatureSubmittedEvent.Digest[:], depositDigest[:]) { + // We add 27 to the recovery ID to align it with ethereum and + // bitcoin protocols where 27 is added to recovery ID to + // indicate usage of uncompressed public keys. + v := 27 + latestSignatureSubmittedEvent.RecoveryID + + return t.chain.ProvideRedemptionSignature( + depositAddress, + v, + latestSignatureSubmittedEvent.R, + latestSignatureSubmittedEvent.S, + ) + } + + return fmt.Errorf( + "could not find signature for digest: [%v]", + depositDigest, + ) + } + + monitoringSubscription, err := t.monitorAndAct( + ctx, + "provide redemption signature", + monitoringStartFn, + monitoringStopFn, + t.watchKeepClosed, + actFn, + actBackoffFn, + func(_ string) (time.Duration, error) { + return timeout, nil + }, + ) + if err != nil { + return err + } + + go func() { + <-ctx.Done() + monitoringSubscription.Unsubscribe() + logger.Infof("provide redemption signature monitoring disabled") + }() + + logger.Infof("provide redemption signature monitoring initialized") + + return nil +} + +func (t *tbtc) monitorProvideRedemptionProof( + ctx context.Context, + actBackoffFn backoffFn, + timeout time.Duration, +) error { + monitoringStartFn := func( + handler depositEventHandler, + ) (subscription.EventSubscription, error) { + // Start right after a redemption signature has been provided. + return t.chain.OnDepositGotRedemptionSignature(handler) + } + + monitoringStopFn := func( + handler depositEventHandler, + ) (subscription.EventSubscription, error) { + // Stop in case the redemption fee has been increased by someone else. + redemptionRequestedSubscription, err := t.chain.OnDepositRedemptionRequested( + handler, + ) + if err != nil { + return nil, err + } + + // Stop in case the redemption proof has been provided by someone else. + redeemedSubscription, err := t.chain.OnDepositRedeemed(handler) + if err != nil { + return nil, err + } + + return subscription.NewEventSubscription( + func() { + redemptionRequestedSubscription.Unsubscribe() + redeemedSubscription.Unsubscribe() + }, + ), nil + } + + actFn := func(depositAddress string) error { + redemptionRequestedEvents, err := t.chain.PastDepositRedemptionRequestedEvents( + depositAddress, + t.pastEventsLookupStartBlock(), + ) + if err != nil { + return err + } + + if len(redemptionRequestedEvents) == 0 { + return fmt.Errorf( + "no redemption requested events found for deposit: [%v]", + depositAddress, + ) + } + + // TODO: Check whether the redemption proof can be submitted by + // interacting with the BTC chain. If yes, construct and submit + // the proof. If not, try to increase the redemption fee. + + latestRedemptionRequestedEvent := + redemptionRequestedEvents[len(redemptionRequestedEvents)-1] + + previousOutputValue := new(big.Int).Sub( + latestRedemptionRequestedEvent.UtxoValue, + latestRedemptionRequestedEvent.RequestedFee, + ) + + newOutputValue := new(big.Int).Sub( + previousOutputValue, + redemptionRequestedEvents[0].RequestedFee, // initial fee + ) + + return t.chain.IncreaseRedemptionFee( + depositAddress, + toLittleEndianBytes(previousOutputValue), + toLittleEndianBytes(newOutputValue), + ) + } + + timeoutFn := func(depositAddress string) (time.Duration, error) { + // Get the seconds timestamp in the moment when this function is + // invoked. This is when the monitoring starts in response of + // the `GotRedemptionSignature` event. + gotRedemptionSignatureTimestamp := uint64(time.Now().Unix()) + + redemptionRequestedEvents, err := t.chain.PastDepositRedemptionRequestedEvents( + depositAddress, + t.pastEventsLookupStartBlock(), + ) + if err != nil { + return 0, err + } + + if len(redemptionRequestedEvents) == 0 { + return 0, fmt.Errorf( + "no redemption requested events found for deposit: [%v]", + depositAddress, + ) + } + + latestRedemptionRequestedEvent := + redemptionRequestedEvents[len(redemptionRequestedEvents)-1] + + // Get the seconds timestamp for the latest redemption request. + redemptionRequestedTimestamp, err := t.chain.BlockTimestamp( + new(big.Int).SetUint64(latestRedemptionRequestedEvent.BlockNumber), + ) + if err != nil { + return 0, err + } + + // We must shift the constant timeout value by subtracting the time + // elapsed between the redemption request and the redemption signature. + // This way we obtain a value close to the redemption proof timeout + // and it doesn't matter when the redemption signature arrives. + timeoutShift := time.Duration( + gotRedemptionSignatureTimestamp-redemptionRequestedTimestamp, + ) * time.Second + + return timeout - timeoutShift, nil + } + + monitoringSubscription, err := t.monitorAndAct( + ctx, + "provide redemption proof", + monitoringStartFn, + monitoringStopFn, + t.watchKeepClosed, + actFn, + actBackoffFn, + timeoutFn, + ) + if err != nil { + return err + } + + go func() { + <-ctx.Done() + monitoringSubscription.Unsubscribe() + logger.Infof("provide redemption proof monitoring disabled") + }() + + logger.Infof("provide redemption proof monitoring initialized") + + return nil +} + +type depositEventHandler func(depositAddress string) + +type watchDepositEventFn func( + handler depositEventHandler, +) (subscription.EventSubscription, error) + +type watchKeepClosedFn func(depositAddress string) ( + keepClosedChan chan struct{}, + unsubscribe func(), + err error, +) + +type submitDepositTxFn func(depositAddress string) error + +type backoffFn func(iteration int) time.Duration + +type timeoutFn func(depositAddress string) (time.Duration, error) + +// TODO (keep-ecdsa/pull/585#discussion_r513447505): // 1. Filter incoming events by operator interest. // 2. Incoming events deduplication. // 3. Resume monitoring after client restart. +// 4. Handle chain reorgs (keep-ecdsa/pull/585#discussion_r511760283) func (t *tbtc) monitorAndAct( ctx context.Context, monitoringName string, @@ -109,20 +422,20 @@ func (t *tbtc) monitorAndAct( keepClosedFn watchKeepClosedFn, actFn submitDepositTxFn, actBackoffFn backoffFn, - timeout time.Duration, + timeoutFn timeoutFn, ) (subscription.EventSubscription, error) { - handleStartEvent := func(deposit string) { + handleStartEvent := func(depositAddress string) { logger.Infof( "starting [%v] monitoring for deposit [%v]", monitoringName, - deposit, + depositAddress, ) stopEventChan := make(chan struct{}) stopEventSubscription, err := monitoringStopFn( - func(stopEventDeposit string) { - if deposit == stopEventDeposit { + func(stopEventDepositAddress string) { + if depositAddress == stopEventDepositAddress { stopEventChan <- struct{}{} } }, @@ -132,26 +445,40 @@ func (t *tbtc) monitorAndAct( "could not setup stop event handler for [%v] "+ "monitoring for deposit [%v]: [%v]", monitoringName, - deposit, + depositAddress, err, ) return } defer stopEventSubscription.Unsubscribe() - keepClosedChan, keepClosedUnsubscribe, err := keepClosedFn(deposit) + keepClosedChan, keepClosedUnsubscribe, err := keepClosedFn( + depositAddress, + ) if err != nil { logger.Errorf( "could not setup keep closed handler for [%v] "+ "monitoring for deposit [%v]: [%v]", monitoringName, - deposit, + depositAddress, err, ) return } defer keepClosedUnsubscribe() + timeout, err := timeoutFn(depositAddress) + if err != nil { + logger.Errorf( + "could determine timeout value for [%v] "+ + "monitoring for deposit [%v]: [%v]", + monitoringName, + depositAddress, + err, + ) + return + } + timeoutChan := time.After(timeout) actionAttempt := 1 @@ -164,7 +491,7 @@ func (t *tbtc) monitorAndAct( "context is done for [%v] "+ "monitoring for deposit [%v]", monitoringName, - deposit, + depositAddress, ) break monitoring case <-stopEventChan: @@ -172,7 +499,7 @@ func (t *tbtc) monitorAndAct( "stop event occurred for [%v] "+ "monitoring for deposit [%v]", monitoringName, - deposit, + depositAddress, ) break monitoring case <-keepClosedChan: @@ -180,7 +507,7 @@ func (t *tbtc) monitorAndAct( "keep closed event occurred for [%v] "+ "monitoring for deposit [%v]", monitoringName, - deposit, + depositAddress, ) break monitoring case <-timeoutChan: @@ -188,10 +515,10 @@ func (t *tbtc) monitorAndAct( "[%v] not performed in the expected time frame "+ "for deposit [%v]; performing the action", monitoringName, - deposit, + depositAddress, ) - err := actFn(deposit) + err := actFn(depositAddress) if err != nil { if actionAttempt == maxActAttempts { logger.Errorf( @@ -199,7 +526,7 @@ func (t *tbtc) monitorAndAct( "for [%v] monitoring for deposit [%v]: [%v]; "+ "the maximum number of attempts reached", monitoringName, - deposit, + depositAddress, err, ) break monitoring @@ -212,7 +539,7 @@ func (t *tbtc) monitorAndAct( "for [%v] monitoring for deposit [%v]: [%v]; "+ "retrying after: [%v]", monitoringName, - deposit, + depositAddress, err, backoff, ) @@ -228,23 +555,23 @@ func (t *tbtc) monitorAndAct( logger.Infof( "stopped [%v] monitoring for deposit [%v]", monitoringName, - deposit, + depositAddress, ) } return monitoringStartFn( - func(deposit string) { - go handleStartEvent(deposit) + func(depositAddress string) { + go handleStartEvent(depositAddress) }, ) } func (t *tbtc) watchKeepClosed( - deposit string, + depositAddress string, ) (chan struct{}, func(), error) { signalChan := make(chan struct{}) - keepAddress, err := t.chain.KeepAddress(deposit) + keepAddress, err := t.chain.KeepAddress(depositAddress) if err != nil { return nil, nil, err } @@ -277,6 +604,19 @@ func (t *tbtc) watchKeepClosed( return signalChan, unsubscribe, nil } +func (t *tbtc) pastEventsLookupStartBlock() uint64 { + currentBlock, err := t.chain.BlockCounter().CurrentBlock() + if err != nil { + return 0 // if something went wrong, start from block `0` + } + + if currentBlock <= pastEventsLookbackBlocks { + return 0 + } + + return currentBlock - pastEventsLookbackBlocks +} + // Computes the exponential backoff value for given iteration. // For each iteration the result value will be in range: // - iteration 1: [2000ms, 2100ms) @@ -290,3 +630,9 @@ func exponentialBackoff(iteration int) time.Duration { jitterMillis := rand.Intn(100) return time.Duration(int(backoffMillis)+jitterMillis) * time.Millisecond } + +func toLittleEndianBytes(value *big.Int) [8]byte { + var valueBytes [8]byte + binary.LittleEndian.PutUint64(valueBytes[:], value.Uint64()) + return valueBytes +} diff --git a/pkg/extensions/tbtc/tbtc_test.go b/pkg/extensions/tbtc/tbtc_test.go index bd2bb63fb..ec041665b 100644 --- a/pkg/extensions/tbtc/tbtc_test.go +++ b/pkg/extensions/tbtc/tbtc_test.go @@ -4,11 +4,15 @@ import ( "bytes" "context" "fmt" + "math/big" "math/rand" "reflect" "testing" "time" + "github.com/keep-network/keep-ecdsa/pkg/ecdsa" + "github.com/keep-network/keep-ecdsa/pkg/utils/byteutils" + "github.com/ethereum/go-ethereum/common" "github.com/keep-network/keep-ecdsa/pkg/chain/local" ) @@ -19,9 +23,11 @@ const ( ) func TestRetrievePubkey_TimeoutElapsed(t *testing.T) { - ctx := context.Background() - tbtcChain := local.NewTBTCLocalChain() - tbtc := &tbtc{tbtcChain} + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) err := tbtc.monitorRetrievePubKey( ctx, @@ -44,7 +50,8 @@ func TestRetrievePubkey_TimeoutElapsed(t *testing.T) { time.Sleep(2 * timeout) expectedRetrieveSignerPubkeyCalls := 1 - actualRetrieveSignerPubkeyCalls := tbtcChain.Logger().RetrieveSignerPubkeyCalls() + actualRetrieveSignerPubkeyCalls := tbtcChain.Logger(). + RetrieveSignerPubkeyCalls() if expectedRetrieveSignerPubkeyCalls != actualRetrieveSignerPubkeyCalls { t.Errorf( "unexpected number of RetrieveSignerPubkey calls\n"+ @@ -57,7 +64,10 @@ func TestRetrievePubkey_TimeoutElapsed(t *testing.T) { depositPubkey, err := tbtcChain.DepositPubkey(depositAddress) if err != nil { - t.Errorf("unexpected error while fetching deposit pubkey: [%v]", err) + t.Errorf( + "unexpected error while fetching deposit pubkey: [%v]", + err, + ) } if !bytes.Equal(keepPubkey[:], depositPubkey) { @@ -72,9 +82,11 @@ func TestRetrievePubkey_TimeoutElapsed(t *testing.T) { } func TestRetrievePubkey_StopEventOccurred(t *testing.T) { - ctx := context.Background() - tbtcChain := local.NewTBTCLocalChain() - tbtc := &tbtc{tbtcChain} + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) err := tbtc.monitorRetrievePubKey( ctx, @@ -107,7 +119,8 @@ func TestRetrievePubkey_StopEventOccurred(t *testing.T) { time.Sleep(2 * timeout) expectedRetrieveSignerPubkeyCalls := 1 - actualRetrieveSignerPubkeyCalls := tbtcChain.Logger().RetrieveSignerPubkeyCalls() + actualRetrieveSignerPubkeyCalls := tbtcChain.Logger(). + RetrieveSignerPubkeyCalls() if expectedRetrieveSignerPubkeyCalls != actualRetrieveSignerPubkeyCalls { t.Errorf( "unexpected number of RetrieveSignerPubkey calls\n"+ @@ -120,7 +133,10 @@ func TestRetrievePubkey_StopEventOccurred(t *testing.T) { depositPubkey, err := tbtcChain.DepositPubkey(depositAddress) if err != nil { - t.Errorf("unexpected error while fetching deposit pubkey: [%v]", err) + t.Errorf( + "unexpected error while fetching deposit pubkey: [%v]", + err, + ) } if !bytes.Equal(keepPubkey[:], depositPubkey) { @@ -135,9 +151,11 @@ func TestRetrievePubkey_StopEventOccurred(t *testing.T) { } func TestRetrievePubkey_KeepClosedEventOccurred(t *testing.T) { - ctx := context.Background() - tbtcChain := local.NewTBTCLocalChain() - tbtc := &tbtc{tbtcChain} + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) err := tbtc.monitorRetrievePubKey( ctx, @@ -169,7 +187,8 @@ func TestRetrievePubkey_KeepClosedEventOccurred(t *testing.T) { time.Sleep(2 * timeout) expectedRetrieveSignerPubkeyCalls := 0 - actualRetrieveSignerPubkeyCalls := tbtcChain.Logger().RetrieveSignerPubkeyCalls() + actualRetrieveSignerPubkeyCalls := tbtcChain.Logger(). + RetrieveSignerPubkeyCalls() if expectedRetrieveSignerPubkeyCalls != actualRetrieveSignerPubkeyCalls { t.Errorf( "unexpected number of RetrieveSignerPubkey calls\n"+ @@ -182,7 +201,10 @@ func TestRetrievePubkey_KeepClosedEventOccurred(t *testing.T) { _, err = tbtcChain.DepositPubkey(depositAddress) - expectedError := fmt.Errorf("no pubkey for deposit [%v]", depositAddress) + expectedError := fmt.Errorf( + "no pubkey for deposit [%v]", + depositAddress, + ) if !reflect.DeepEqual(expectedError, err) { t.Errorf( "unexpected error\n"+ @@ -195,9 +217,11 @@ func TestRetrievePubkey_KeepClosedEventOccurred(t *testing.T) { } func TestRetrievePubkey_KeepTerminatedEventOccurred(t *testing.T) { - ctx := context.Background() - tbtcChain := local.NewTBTCLocalChain() - tbtc := &tbtc{tbtcChain} + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) err := tbtc.monitorRetrievePubKey( ctx, @@ -229,7 +253,8 @@ func TestRetrievePubkey_KeepTerminatedEventOccurred(t *testing.T) { time.Sleep(2 * timeout) expectedRetrieveSignerPubkeyCalls := 0 - actualRetrieveSignerPubkeyCalls := tbtcChain.Logger().RetrieveSignerPubkeyCalls() + actualRetrieveSignerPubkeyCalls := tbtcChain.Logger(). + RetrieveSignerPubkeyCalls() if expectedRetrieveSignerPubkeyCalls != actualRetrieveSignerPubkeyCalls { t.Errorf( "unexpected number of RetrieveSignerPubkey calls\n"+ @@ -242,7 +267,10 @@ func TestRetrievePubkey_KeepTerminatedEventOccurred(t *testing.T) { _, err = tbtcChain.DepositPubkey(depositAddress) - expectedError := fmt.Errorf("no pubkey for deposit [%v]", depositAddress) + expectedError := fmt.Errorf( + "no pubkey for deposit [%v]", + depositAddress, + ) if !reflect.DeepEqual(expectedError, err) { t.Errorf( "unexpected error\n"+ @@ -255,9 +283,11 @@ func TestRetrievePubkey_KeepTerminatedEventOccurred(t *testing.T) { } func TestRetrievePubkey_ActionFailed(t *testing.T) { - ctx := context.Background() - tbtcChain := local.NewTBTCLocalChain() - tbtc := &tbtc{tbtcChain} + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) err := tbtc.monitorRetrievePubKey( ctx, @@ -278,7 +308,8 @@ func TestRetrievePubkey_ActionFailed(t *testing.T) { time.Sleep(2 * timeout) expectedRetrieveSignerPubkeyCalls := 3 - actualRetrieveSignerPubkeyCalls := tbtcChain.Logger().RetrieveSignerPubkeyCalls() + actualRetrieveSignerPubkeyCalls := tbtcChain.Logger(). + RetrieveSignerPubkeyCalls() if expectedRetrieveSignerPubkeyCalls != actualRetrieveSignerPubkeyCalls { t.Errorf( "unexpected number of RetrieveSignerPubkey calls\n"+ @@ -292,8 +323,10 @@ func TestRetrievePubkey_ActionFailed(t *testing.T) { func TestRetrievePubkey_ContextCancelled_WithoutWorkingMonitoring(t *testing.T) { ctx, cancelCtx := context.WithCancel(context.Background()) - tbtcChain := local.NewTBTCLocalChain() - tbtc := &tbtc{tbtcChain} + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) err := tbtc.monitorRetrievePubKey( ctx, @@ -314,7 +347,8 @@ func TestRetrievePubkey_ContextCancelled_WithoutWorkingMonitoring(t *testing.T) time.Sleep(2 * timeout) expectedRetrieveSignerPubkeyCalls := 0 - actualRetrieveSignerPubkeyCalls := tbtcChain.Logger().RetrieveSignerPubkeyCalls() + actualRetrieveSignerPubkeyCalls := tbtcChain.Logger(). + RetrieveSignerPubkeyCalls() if expectedRetrieveSignerPubkeyCalls != actualRetrieveSignerPubkeyCalls { t.Errorf( "unexpected number of RetrieveSignerPubkey calls\n"+ @@ -328,8 +362,10 @@ func TestRetrievePubkey_ContextCancelled_WithoutWorkingMonitoring(t *testing.T) func TestRetrievePubkey_ContextCancelled_WithWorkingMonitoring(t *testing.T) { ctx, cancelCtx := context.WithCancel(context.Background()) - tbtcChain := local.NewTBTCLocalChain() - tbtc := &tbtc{tbtcChain} + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) err := tbtc.monitorRetrievePubKey( ctx, @@ -355,8 +391,10 @@ func TestRetrievePubkey_ContextCancelled_WithWorkingMonitoring(t *testing.T) { time.Sleep(2 * timeout) expectedRetrieveSignerPubkeyCalls := 0 - actualRetrieveSignerPubkeyCalls := tbtcChain.Logger().RetrieveSignerPubkeyCalls() - if expectedRetrieveSignerPubkeyCalls != actualRetrieveSignerPubkeyCalls { + actualRetrieveSignerPubkeyCalls := tbtcChain.Logger(). + RetrieveSignerPubkeyCalls() + if expectedRetrieveSignerPubkeyCalls != + actualRetrieveSignerPubkeyCalls { t.Errorf( "unexpected number of RetrieveSignerPubkey calls\n"+ "expected: [%v]\n"+ @@ -367,27 +405,1329 @@ func TestRetrievePubkey_ContextCancelled_WithWorkingMonitoring(t *testing.T) { } } -func submitKeepPublicKey( - depositAddress string, - tbtcChain *local.TBTCLocalChain, -) ([64]byte, error) { - keepAddress, err := tbtcChain.KeepAddress(depositAddress) +func TestProvideRedemptionSignature_TimeoutElapsed(t *testing.T) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) + + err := tbtc.monitorProvideRedemptionSignature( + ctx, + constantBackoff, + timeout, + ) if err != nil { - return [64]byte{}, err + t.Fatal(err) } - var keepPubkey [64]byte - rand.Read(keepPubkey[:]) + tbtcChain.CreateDeposit(depositAddress) - err = tbtcChain.SubmitKeepPublicKey( - common.HexToAddress(keepAddress), - keepPubkey, + _, err = submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.RedeemDeposit(depositAddress) + if err != nil { + t.Fatal(err) + } + + keepSignature, err := submitKeepSignature(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) + + expectedProvideRedemptionSignatureCalls := 1 + actualProvideRedemptionSignatureCalls := tbtcChain.Logger(). + ProvideRedemptionSignatureCalls() + if expectedProvideRedemptionSignatureCalls != + actualProvideRedemptionSignatureCalls { + t.Errorf( + "unexpected number of ProvideRedemptionSignature calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedProvideRedemptionSignatureCalls, + actualProvideRedemptionSignatureCalls, + ) + } + + depositSignature, err := tbtcChain.DepositRedemptionSignature( + depositAddress, ) if err != nil { - return [64]byte{}, err + t.Errorf( + "unexpected error while fetching deposit signature: [%v]", + err, + ) } - return keepPubkey, nil + if !areChainSignaturesEqual(keepSignature, depositSignature) { + t.Errorf( + "unexpected signature\n"+ + "expected: [%+v]\n"+ + "actual: [%+v]", + keepSignature, + depositSignature, + ) + } +} + +func TestProvideRedemptionSignature_StopEventOccurred_DepositGotRedemptionSignature( + t *testing.T, +) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) + + err := tbtc.monitorProvideRedemptionSignature( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.CreateDeposit(depositAddress) + + _, err = submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.RedeemDeposit(depositAddress) + if err != nil { + t.Fatal(err) + } + + keepSignature, err := submitKeepSignature(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.ProvideRedemptionSignature( + depositAddress, + keepSignature.V, + keepSignature.R, + keepSignature.S, + ) + 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) + + expectedProvideRedemptionSignatureCalls := 1 + actualProvideRedemptionSignatureCalls := tbtcChain.Logger(). + ProvideRedemptionSignatureCalls() + if expectedProvideRedemptionSignatureCalls != + actualProvideRedemptionSignatureCalls { + t.Errorf( + "unexpected number of ProvideRedemptionSignature calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedProvideRedemptionSignatureCalls, + actualProvideRedemptionSignatureCalls, + ) + } + + depositSignature, err := tbtcChain.DepositRedemptionSignature( + depositAddress, + ) + if err != nil { + t.Errorf( + "unexpected error while fetching deposit signature: [%v]", + err, + ) + } + + if !areChainSignaturesEqual(keepSignature, depositSignature) { + t.Errorf( + "unexpected signature\n"+ + "expected: [%+v]\n"+ + "actual: [%+v]", + keepSignature, + depositSignature, + ) + } +} + +func TestProvideRedemptionSignature_StopEventOccurred_DepositRedeemed( + t *testing.T, +) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) + + err := tbtc.monitorProvideRedemptionSignature( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.CreateDeposit(depositAddress) + + _, err = submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.RedeemDeposit(depositAddress) + if err != nil { + t.Fatal(err) + } + + _, err = submitKeepSignature(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.ProvideRedemptionProof( + depositAddress, + [4]uint8{}, + nil, + nil, + [4]uint8{}, + nil, + nil, + nil, + ) + 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) + + expectedProvideRedemptionSignatureCalls := 0 + actualProvideRedemptionSignatureCalls := tbtcChain.Logger(). + ProvideRedemptionSignatureCalls() + if expectedProvideRedemptionSignatureCalls != + actualProvideRedemptionSignatureCalls { + t.Errorf( + "unexpected number of ProvideRedemptionSignature calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedProvideRedemptionSignatureCalls, + actualProvideRedemptionSignatureCalls, + ) + } + + depositProof, err := tbtcChain.DepositRedemptionProof(depositAddress) + if err != nil { + t.Errorf("unexpected error while fetching deposit proof: [%v]", err) + } + + if depositProof == nil { + t.Errorf("deposit proof should be provided") + } +} + +func TestProvideRedemptionSignature_KeepClosedEventOccurred(t *testing.T) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) + + err := tbtc.monitorProvideRedemptionSignature( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.CreateDeposit(depositAddress) + + _, err = submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.RedeemDeposit(depositAddress) + if err != nil { + t.Fatal(err) + } + + _, err = submitKeepSignature(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) + + expectedProvideRedemptionSignatureCalls := 0 + actualProvideRedemptionSignatureCalls := tbtcChain.Logger(). + ProvideRedemptionSignatureCalls() + if expectedProvideRedemptionSignatureCalls != + actualProvideRedemptionSignatureCalls { + t.Errorf( + "unexpected number of ProvideRedemptionSignature calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedProvideRedemptionSignatureCalls, + actualProvideRedemptionSignatureCalls, + ) + } + + _, err = tbtcChain.DepositRedemptionSignature(depositAddress) + + expectedError := fmt.Errorf( + "no redemption signature for deposit [%v]", + depositAddress, + ) + if !reflect.DeepEqual(expectedError, err) { + t.Errorf( + "unexpected error\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedError, + err, + ) + } +} + +func TestProvideRedemptionSignature_KeepTerminatedEventOccurred(t *testing.T) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) + + err := tbtc.monitorProvideRedemptionSignature( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.CreateDeposit(depositAddress) + + _, err = submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.RedeemDeposit(depositAddress) + if err != nil { + t.Fatal(err) + } + + _, err = submitKeepSignature(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) + + expectedProvideRedemptionSignatureCalls := 0 + actualProvideRedemptionSignatureCalls := tbtcChain.Logger(). + ProvideRedemptionSignatureCalls() + if expectedProvideRedemptionSignatureCalls != + actualProvideRedemptionSignatureCalls { + t.Errorf( + "unexpected number of ProvideRedemptionSignature calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedProvideRedemptionSignatureCalls, + actualProvideRedemptionSignatureCalls, + ) + } + + _, err = tbtcChain.DepositRedemptionSignature(depositAddress) + + expectedError := fmt.Errorf( + "no redemption signature for deposit [%v]", + depositAddress, + ) + if !reflect.DeepEqual(expectedError, err) { + t.Errorf( + "unexpected error\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedError, + err, + ) + } +} + +func TestProvideRedemptionSignature_ActionFailed(t *testing.T) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) + + err := tbtc.monitorProvideRedemptionSignature( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.CreateDeposit(depositAddress) + + _, err = submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.RedeemDeposit(depositAddress) + if err != nil { + t.Fatal(err) + } + + _, err = submitKeepSignature(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + // simulate a situation when `ProvideRedemptionSignature` fails on-chain + tbtcChain.SetAlwaysFailingTransactions("ProvideRedemptionSignature") + + // wait a bit longer than the monitoring timeout + // to make sure the potential transaction completes + time.Sleep(2 * timeout) + + expectedProvideRedemptionSignatureCalls := 3 + actualProvideRedemptionSignatureCalls := tbtcChain.Logger(). + ProvideRedemptionSignatureCalls() + if expectedProvideRedemptionSignatureCalls != + actualProvideRedemptionSignatureCalls { + t.Errorf( + "unexpected number of ProvideRedemptionSignature calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedProvideRedemptionSignatureCalls, + actualProvideRedemptionSignatureCalls, + ) + } +} + +func TestProvideRedemptionSignature_ContextCancelled_WithoutWorkingMonitoring( + t *testing.T, +) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) + + err := tbtc.monitorProvideRedemptionSignature( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + // cancel the context before any start event occurs + cancelCtx() + + tbtcChain.CreateDeposit(depositAddress) + + _, err = submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.RedeemDeposit(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) + + expectedProvideRedemptionSignatureCalls := 0 + actualProvideRedemptionSignatureCalls := tbtcChain.Logger(). + ProvideRedemptionSignatureCalls() + if expectedProvideRedemptionSignatureCalls != + actualProvideRedemptionSignatureCalls { + t.Errorf( + "unexpected number of ProvideRedemptionSignature calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedProvideRedemptionSignatureCalls, + actualProvideRedemptionSignatureCalls, + ) + } +} + +func TestProvideRedemptionSignature_ContextCancelled_WithWorkingMonitoring( + t *testing.T, +) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) + + err := tbtc.monitorProvideRedemptionSignature( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.CreateDeposit(depositAddress) + + _, err = submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.RedeemDeposit(depositAddress) + if err != nil { + t.Fatal(err) + } + + // 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) + + expectedProvideRedemptionSignatureCalls := 0 + actualProvideRedemptionSignatureCalls := tbtcChain.Logger(). + ProvideRedemptionSignatureCalls() + if expectedProvideRedemptionSignatureCalls != + actualProvideRedemptionSignatureCalls { + t.Errorf( + "unexpected number of ProvideRedemptionSignature calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedProvideRedemptionSignatureCalls, + actualProvideRedemptionSignatureCalls, + ) + } +} + +func TestProvideRedemptionProof_TimeoutElapsed(t *testing.T) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) + + err := tbtc.monitorProvideRedemptionProof( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.CreateDeposit(depositAddress) + + _, err = submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.RedeemDeposit(depositAddress) + if err != nil { + t.Fatal(err) + } + + initialDepositRedemptionFee, err := tbtcChain.DepositRedemptionFee( + depositAddress, + ) + if err != nil { + t.Fatal(err) + } + + keepSignature, err := submitKeepSignature(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.ProvideRedemptionSignature( + depositAddress, + keepSignature.V, + keepSignature.R, + keepSignature.S, + ) + 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) + + expectedIncreaseRedemptionFeeCalls := 1 + actualIncreaseRedemptionFeeCalls := tbtcChain.Logger(). + IncreaseRedemptionFeeCalls() + if expectedIncreaseRedemptionFeeCalls != actualIncreaseRedemptionFeeCalls { + t.Errorf( + "unexpected number of IncreaseRedemptionFee calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedIncreaseRedemptionFeeCalls, + actualIncreaseRedemptionFeeCalls, + ) + } + + expectedDepositRedemptionFee := new(big.Int).Mul( + big.NewInt(2), + initialDepositRedemptionFee, + ) + + actualDepositRedemptionFee, err := tbtcChain.DepositRedemptionFee( + depositAddress, + ) + if err != nil { + t.Fatal(err) + } + + if expectedDepositRedemptionFee.Cmp(actualDepositRedemptionFee) != 0 { + t.Errorf( + "unexpected redemption fee value\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedDepositRedemptionFee.Text(10), + actualDepositRedemptionFee.Text(10), + ) + } +} + +func TestProvideRedemptionProof_StopEventOccurred_DepositRedemptionRequested( + t *testing.T, +) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) + + err := tbtc.monitorProvideRedemptionProof( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.CreateDeposit(depositAddress) + + _, err = submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.RedeemDeposit(depositAddress) + if err != nil { + t.Fatal(err) + } + + initialDepositRedemptionFee, err := tbtcChain.DepositRedemptionFee( + depositAddress, + ) + if err != nil { + t.Fatal(err) + } + + keepSignature, err := submitKeepSignature(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.ProvideRedemptionSignature( + depositAddress, + keepSignature.V, + keepSignature.R, + keepSignature.S, + ) + 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.IncreaseRedemptionFee( + depositAddress, + toLittleEndianBytes(big.NewInt(990)), + toLittleEndianBytes(big.NewInt(980)), + ) + 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) + + // Expect exactly one call of `IncreaseRedemptionFee` coming from the + // explicit invocation placed above. The monitoring routine is not expected + // to do any calls. + expectedIncreaseRedemptionFeeCalls := 1 + actualIncreaseRedemptionFeeCalls := tbtcChain.Logger(). + IncreaseRedemptionFeeCalls() + if expectedIncreaseRedemptionFeeCalls != actualIncreaseRedemptionFeeCalls { + t.Errorf( + "unexpected number of IncreaseRedemptionFee calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedIncreaseRedemptionFeeCalls, + actualIncreaseRedemptionFeeCalls, + ) + } + + expectedDepositRedemptionFee := new(big.Int).Mul( + big.NewInt(2), + initialDepositRedemptionFee, + ) + + actualDepositRedemptionFee, err := tbtcChain.DepositRedemptionFee( + depositAddress, + ) + if err != nil { + t.Fatal(err) + } + + if expectedDepositRedemptionFee.Cmp(actualDepositRedemptionFee) != 0 { + t.Errorf( + "unexpected redemption fee value\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedDepositRedemptionFee.Text(10), + actualDepositRedemptionFee.Text(10), + ) + } +} + +func TestProvideRedemptionProof_StopEventOccurred_DepositRedeemed( + t *testing.T, +) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) + + err := tbtc.monitorProvideRedemptionProof( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.CreateDeposit(depositAddress) + + _, err = submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.RedeemDeposit(depositAddress) + if err != nil { + t.Fatal(err) + } + + keepSignature, err := submitKeepSignature(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.ProvideRedemptionSignature( + depositAddress, + keepSignature.V, + keepSignature.R, + keepSignature.S, + ) + 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.ProvideRedemptionProof( + depositAddress, + [4]uint8{}, + nil, + nil, + [4]uint8{}, + nil, + nil, + nil, + ) + 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) + + expectedIncreaseRedemptionFeeCalls := 0 + actualIncreaseRedemptionFeeCalls := tbtcChain.Logger(). + IncreaseRedemptionFeeCalls() + if expectedIncreaseRedemptionFeeCalls != actualIncreaseRedemptionFeeCalls { + t.Errorf( + "unexpected number of IncreaseRedemptionFee calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedIncreaseRedemptionFeeCalls, + actualIncreaseRedemptionFeeCalls, + ) + } + + depositProof, err := tbtcChain.DepositRedemptionProof(depositAddress) + if err != nil { + t.Errorf("unexpected error while fetching deposit proof: [%v]", err) + } + + if depositProof == nil { + t.Errorf("deposit proof should be provided") + } +} + +func TestProvideRedemptionProof_KeepClosedEventOccurred(t *testing.T) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) + + err := tbtc.monitorProvideRedemptionProof( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.CreateDeposit(depositAddress) + + _, err = submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.RedeemDeposit(depositAddress) + if err != nil { + t.Fatal(err) + } + + initialDepositRedemptionFee, err := tbtcChain.DepositRedemptionFee( + depositAddress, + ) + if err != nil { + t.Fatal(err) + } + + keepSignature, err := submitKeepSignature(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.ProvideRedemptionSignature( + depositAddress, + keepSignature.V, + keepSignature.R, + keepSignature.S, + ) + 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) + + expectedIncreaseRedemptionFeeCalls := 0 + actualIncreaseRedemptionFeeCalls := tbtcChain.Logger(). + IncreaseRedemptionFeeCalls() + if expectedIncreaseRedemptionFeeCalls != actualIncreaseRedemptionFeeCalls { + t.Errorf( + "unexpected number of IncreaseRedemptionFee calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedIncreaseRedemptionFeeCalls, + actualIncreaseRedemptionFeeCalls, + ) + } + + actualDepositRedemptionFee, err := tbtcChain.DepositRedemptionFee( + depositAddress, + ) + if err != nil { + t.Fatal(err) + } + + if initialDepositRedemptionFee.Cmp(actualDepositRedemptionFee) != 0 { + t.Errorf( + "unexpected redemption fee value\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + initialDepositRedemptionFee.Text(10), + actualDepositRedemptionFee.Text(10), + ) + } +} + +func TestProvideRedemptionProof_KeepTerminatedEventOccurred(t *testing.T) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) + + err := tbtc.monitorProvideRedemptionProof( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.CreateDeposit(depositAddress) + + _, err = submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.RedeemDeposit(depositAddress) + if err != nil { + t.Fatal(err) + } + + initialDepositRedemptionFee, err := tbtcChain.DepositRedemptionFee( + depositAddress, + ) + if err != nil { + t.Fatal(err) + } + + keepSignature, err := submitKeepSignature(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.ProvideRedemptionSignature( + depositAddress, + keepSignature.V, + keepSignature.R, + keepSignature.S, + ) + 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) + + expectedIncreaseRedemptionFeeCalls := 0 + actualIncreaseRedemptionFeeCalls := tbtcChain.Logger(). + IncreaseRedemptionFeeCalls() + if expectedIncreaseRedemptionFeeCalls != actualIncreaseRedemptionFeeCalls { + t.Errorf( + "unexpected number of IncreaseRedemptionFee calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedIncreaseRedemptionFeeCalls, + actualIncreaseRedemptionFeeCalls, + ) + } + + actualDepositRedemptionFee, err := tbtcChain.DepositRedemptionFee( + depositAddress, + ) + if err != nil { + t.Fatal(err) + } + + if initialDepositRedemptionFee.Cmp(actualDepositRedemptionFee) != 0 { + t.Errorf( + "unexpected redemption fee value\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + initialDepositRedemptionFee.Text(10), + actualDepositRedemptionFee.Text(10), + ) + } +} + +func TestProvideRedemptionProof_ActionFailed(t *testing.T) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) + + err := tbtc.monitorProvideRedemptionProof( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.CreateDeposit(depositAddress) + + _, err = submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.RedeemDeposit(depositAddress) + if err != nil { + t.Fatal(err) + } + + keepSignature, err := submitKeepSignature(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.ProvideRedemptionSignature( + depositAddress, + keepSignature.V, + keepSignature.R, + keepSignature.S, + ) + if err != nil { + t.Fatal(err) + } + + // simulate a situation when `IncreaseRedemptionFee` fails on-chain + tbtcChain.SetAlwaysFailingTransactions("IncreaseRedemptionFee") + + // wait a bit longer than the monitoring timeout + // to make sure the potential transaction completes + time.Sleep(2 * timeout) + + expectedIncreaseRedemptionFeeCalls := 3 + actualIncreaseRedemptionFeeCalls := tbtcChain.Logger(). + IncreaseRedemptionFeeCalls() + if expectedIncreaseRedemptionFeeCalls != actualIncreaseRedemptionFeeCalls { + t.Errorf( + "unexpected number of IncreaseRedemptionFee calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedIncreaseRedemptionFeeCalls, + actualIncreaseRedemptionFeeCalls, + ) + } +} + +func TestProvideRedemptionProof_ContextCancelled_WithoutWorkingMonitoring( + t *testing.T, +) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) + + err := tbtc.monitorProvideRedemptionProof( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + // cancel the context before any start event occurs + cancelCtx() + + tbtcChain.CreateDeposit(depositAddress) + + _, err = submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.RedeemDeposit(depositAddress) + if err != nil { + t.Fatal(err) + } + + keepSignature, err := submitKeepSignature(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.ProvideRedemptionSignature( + depositAddress, + keepSignature.V, + keepSignature.R, + keepSignature.S, + ) + 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) + + expectedIncreaseRedemptionFeeCalls := 0 + actualIncreaseRedemptionFeeCalls := tbtcChain.Logger(). + IncreaseRedemptionFeeCalls() + if expectedIncreaseRedemptionFeeCalls != actualIncreaseRedemptionFeeCalls { + t.Errorf( + "unexpected number of IncreaseRedemptionFee calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedIncreaseRedemptionFeeCalls, + actualIncreaseRedemptionFeeCalls, + ) + } +} + +func TestProvideRedemptionProof_ContextCancelled_WithWorkingMonitoring( + t *testing.T, +) { + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + tbtcChain := local.NewTBTCLocalChain(ctx) + tbtc := newTBTC(tbtcChain) + + err := tbtc.monitorProvideRedemptionProof( + ctx, + constantBackoff, + timeout, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.CreateDeposit(depositAddress) + + _, err = submitKeepPublicKey(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.RedeemDeposit(depositAddress) + if err != nil { + t.Fatal(err) + } + + keepSignature, err := submitKeepSignature(depositAddress, tbtcChain) + if err != nil { + t.Fatal(err) + } + + err = tbtcChain.ProvideRedemptionSignature( + depositAddress, + keepSignature.V, + keepSignature.R, + keepSignature.S, + ) + if err != nil { + t.Fatal(err) + } + + // 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) + + expectedIncreaseRedemptionFeeCalls := 0 + actualIncreaseRedemptionFeeCalls := tbtcChain.Logger(). + IncreaseRedemptionFeeCalls() + if expectedIncreaseRedemptionFeeCalls != actualIncreaseRedemptionFeeCalls { + t.Errorf( + "unexpected number of IncreaseRedemptionFee calls\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedIncreaseRedemptionFeeCalls, + actualIncreaseRedemptionFeeCalls, + ) + } +} + +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 submitKeepSignature( + depositAddress string, + tbtcChain *local.TBTCLocalChain, +) (*local.Signature, error) { + keepAddress, err := tbtcChain.KeepAddress(depositAddress) + if err != nil { + return nil, err + } + + signature := &ecdsa.Signature{ + R: new(big.Int).SetUint64(rand.Uint64()), + S: new(big.Int).SetUint64(rand.Uint64()), + RecoveryID: rand.Intn(4), + } + + err = tbtcChain.SubmitSignature( + common.HexToAddress(keepAddress), + signature, + ) + if err != nil { + return nil, err + } + + return toChainSignature(signature) +} + +func toChainSignature(signature *ecdsa.Signature) (*local.Signature, error) { + v := uint8(27 + signature.RecoveryID) + + r, err := byteutils.BytesTo32Byte(signature.R.Bytes()) + if err != nil { + return nil, err + } + + s, err := byteutils.BytesTo32Byte(signature.S.Bytes()) + if err != nil { + return nil, err + } + + return &local.Signature{ + V: v, + R: r, + S: s, + }, nil +} + +func areChainSignaturesEqual(signature1, signature2 *local.Signature) bool { + if signature1.V != signature2.V { + return false + } + + if !bytes.Equal(signature1.R[:], signature2.R[:]) { + return false + } + + if !bytes.Equal(signature1.S[:], signature2.S[:]) { + return false + } + + return true } func closeKeep( diff --git a/pkg/firewall/firewall_test.go b/pkg/firewall/firewall_test.go index bb25f6a54..da77ac876 100644 --- a/pkg/firewall/firewall_test.go +++ b/pkg/firewall/firewall_test.go @@ -1,6 +1,7 @@ package firewall import ( + "context" "crypto/ecdsa" "fmt" "reflect" @@ -20,7 +21,10 @@ var cacheLifeTime = time.Second // Has minimum stake. // Should allow to connect. func TestHasMinimumStake(t *testing.T) { - chain := local.Connect() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := local.Connect(ctx) coreFirewall := newMockCoreFirewall() policy := createNewPolicy(chain, coreFirewall) @@ -42,7 +46,10 @@ func TestHasMinimumStake(t *testing.T) { // Has no authorization. // Should NOT allow to connect. func TestNoAuthorization(t *testing.T) { - chain := local.Connect() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := local.Connect(ctx) coreFirewall := newMockCoreFirewall() policy := createNewPolicy(chain, coreFirewall) @@ -66,7 +73,10 @@ func TestNoAuthorization(t *testing.T) { // Has no authorization // Should cache the information operator is not authorized func TestCachesNotAuthorizedOperators(t *testing.T) { - chain := local.Connect() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := local.Connect(ctx) coreFirewall := newMockCoreFirewall() policy := createNewPolicy(chain, coreFirewall) @@ -93,7 +103,10 @@ func TestCachesNotAuthorizedOperators(t *testing.T) { // Has authorization // Should cache the information operator is authorized. func TestCachesAuthorizedOperators(t *testing.T) { - chain := local.Connect() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := local.Connect(ctx) coreFirewall := newMockCoreFirewall() policy := createNewPolicy(chain, coreFirewall) @@ -119,7 +132,10 @@ func TestCachesAuthorizedOperators(t *testing.T) { } func TestConsultsAuthorizedOperatorsCache(t *testing.T) { - chain := local.Connect() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := local.Connect(ctx) coreFirewall := newMockCoreFirewall() policy := createNewPolicy(chain, coreFirewall) @@ -160,7 +176,10 @@ func TestConsultsAuthorizedOperatorsCache(t *testing.T) { // No keeps exist. // Should NOT allow to connect. func TestNoMinimumStakeNoKeepsExist(t *testing.T) { - chain := local.Connect() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := local.Connect(ctx) coreFirewall := newMockCoreFirewall() policy := createNewPolicy(chain, coreFirewall) @@ -189,7 +208,10 @@ func TestNoMinimumStakeNoKeepsExist(t *testing.T) { // It not a member of a keep. // Should NOT allow to connect. func TestNoMinimumStakeIsNotKeepMember(t *testing.T) { - chain := local.Connect() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := local.Connect(ctx) coreFirewall := newMockCoreFirewall() policy := createNewPolicy(chain, coreFirewall) @@ -226,7 +248,10 @@ func TestNoMinimumStakeIsNotKeepMember(t *testing.T) { // Is a member of an active keep // Should allow to connect. func TestNoMinimumStakeIsActiveKeepMember(t *testing.T) { - chain := local.Connect() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := local.Connect(ctx) coreFirewall := newMockCoreFirewall() policy := createNewPolicy(chain, coreFirewall) @@ -259,7 +284,10 @@ func TestNoMinimumStakeIsActiveKeepMember(t *testing.T) { // Is a member of a closed keep // Should NOT allow to connect. func TestNoMinimumStakeIsClosedKeepMember(t *testing.T) { - chain := local.Connect() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := local.Connect(ctx) coreFirewall := newMockCoreFirewall() policy := createNewPolicy(chain, coreFirewall) @@ -301,7 +329,10 @@ func TestNoMinimumStakeIsClosedKeepMember(t *testing.T) { // Is a member of an active keep // Should allow to connect. func TestNoMinimumStakeMultipleKeepsMember(t *testing.T) { - chain := local.Connect() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := local.Connect(ctx) coreFirewall := newMockCoreFirewall() policy := createNewPolicy(chain, coreFirewall) @@ -348,7 +379,10 @@ func TestNoMinimumStakeMultipleKeepsMember(t *testing.T) { // Is not a member of an active keep. // Should NOT allow to connect but should cache all active keep members in-memory. func TestCachesAllActiveKeepMembers(t *testing.T) { - chain := local.Connect() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := local.Connect(ctx) coreFirewall := newMockCoreFirewall() policy := createNewPolicy(chain, coreFirewall) @@ -425,7 +459,10 @@ func TestCachesAllActiveKeepMembers(t *testing.T) { // After some time, the keep gets closed. // It should no longer allow to connect. func TestSweepsActiveKeepMembersCache(t *testing.T) { - chain := local.Connect() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := local.Connect(ctx) coreFirewall := newMockCoreFirewall() policy := createNewPolicy(chain, coreFirewall) @@ -485,7 +522,10 @@ func TestSweepsActiveKeepMembersCache(t *testing.T) { // Shortly after that, the minimum stake drops below the required minimum but // the membership in an active keep remains. func TestSweepsNoActiveKeepMembersCache(t *testing.T) { - chain := local.Connect() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := local.Connect(ctx) coreFirewall := newMockCoreFirewall() policy := createNewPolicy(chain, coreFirewall) @@ -538,7 +578,10 @@ func TestSweepsNoActiveKeepMembersCache(t *testing.T) { } func TestIsKeepActiveCaching(t *testing.T) { - chain := local.Connect() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := local.Connect(ctx) coreFirewall := newMockCoreFirewall() policy := createNewPolicy(chain, coreFirewall) @@ -611,7 +654,10 @@ func TestIsKeepActiveCaching(t *testing.T) { } func TestGetKeepMembersCaching(t *testing.T) { - chain := local.Connect() + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + chain := local.Connect(ctx) coreFirewall := newMockCoreFirewall() policy := createNewPolicy(chain, coreFirewall)