From 126198228ea8d689d895e6fb51390cdac4c61907 Mon Sep 17 00:00:00 2001 From: KonradStaniec Date: Fri, 21 Jun 2024 16:38:34 +0200 Subject: [PATCH] Verify covenant member signature --- internal/mocks/expected_interfaces_mocks.go | 283 +++++++++++++++++++ internal/services/expected_interfaces.go | 4 - internal/services/remote_signer.go | 2 +- internal/services/unbonding_pipeline.go | 44 ++- internal/services/unbonding_pipeline_test.go | 252 +++++++++++++++++ 5 files changed, 572 insertions(+), 13 deletions(-) create mode 100644 internal/mocks/expected_interfaces_mocks.go create mode 100644 internal/services/unbonding_pipeline_test.go diff --git a/internal/mocks/expected_interfaces_mocks.go b/internal/mocks/expected_interfaces_mocks.go new file mode 100644 index 0000000..c410e28 --- /dev/null +++ b/internal/mocks/expected_interfaces_mocks.go @@ -0,0 +1,283 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/services/expected_interfaces.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + services "github.com/babylonchain/cli-tools/internal/services" + chainhash "github.com/btcsuite/btcd/chaincfg/chainhash" + wire "github.com/btcsuite/btcd/wire" + gomock "github.com/golang/mock/gomock" +) + +// MockCovenantSigner is a mock of CovenantSigner interface. +type MockCovenantSigner struct { + ctrl *gomock.Controller + recorder *MockCovenantSignerMockRecorder +} + +// MockCovenantSignerMockRecorder is the mock recorder for MockCovenantSigner. +type MockCovenantSignerMockRecorder struct { + mock *MockCovenantSigner +} + +// NewMockCovenantSigner creates a new mock instance. +func NewMockCovenantSigner(ctrl *gomock.Controller) *MockCovenantSigner { + mock := &MockCovenantSigner{ctrl: ctrl} + mock.recorder = &MockCovenantSignerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCovenantSigner) EXPECT() *MockCovenantSignerMockRecorder { + return m.recorder +} + +// SignUnbondingTransaction mocks base method. +func (m *MockCovenantSigner) SignUnbondingTransaction(req *services.SignRequest) (*services.PubKeySigPair, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SignUnbondingTransaction", req) + ret0, _ := ret[0].(*services.PubKeySigPair) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SignUnbondingTransaction indicates an expected call of SignUnbondingTransaction. +func (mr *MockCovenantSignerMockRecorder) SignUnbondingTransaction(req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignUnbondingTransaction", reflect.TypeOf((*MockCovenantSigner)(nil).SignUnbondingTransaction), req) +} + +// MockBtcSender is a mock of BtcSender interface. +type MockBtcSender struct { + ctrl *gomock.Controller + recorder *MockBtcSenderMockRecorder +} + +// MockBtcSenderMockRecorder is the mock recorder for MockBtcSender. +type MockBtcSenderMockRecorder struct { + mock *MockBtcSender +} + +// NewMockBtcSender creates a new mock instance. +func NewMockBtcSender(ctrl *gomock.Controller) *MockBtcSender { + mock := &MockBtcSender{ctrl: ctrl} + mock.recorder = &MockBtcSenderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBtcSender) EXPECT() *MockBtcSenderMockRecorder { + return m.recorder +} + +// CheckTxOutSpendable mocks base method. +func (m *MockBtcSender) CheckTxOutSpendable(txHash *chainhash.Hash, index uint32, mempool bool) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckTxOutSpendable", txHash, index, mempool) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CheckTxOutSpendable indicates an expected call of CheckTxOutSpendable. +func (mr *MockBtcSenderMockRecorder) CheckTxOutSpendable(txHash, index, mempool interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckTxOutSpendable", reflect.TypeOf((*MockBtcSender)(nil).CheckTxOutSpendable), txHash, index, mempool) +} + +// SendTx mocks base method. +func (m *MockBtcSender) SendTx(tx *wire.MsgTx) (*chainhash.Hash, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendTx", tx) + ret0, _ := ret[0].(*chainhash.Hash) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SendTx indicates an expected call of SendTx. +func (mr *MockBtcSenderMockRecorder) SendTx(tx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendTx", reflect.TypeOf((*MockBtcSender)(nil).SendTx), tx) +} + +// TxByHash mocks base method. +func (m *MockBtcSender) TxByHash(txHash *chainhash.Hash, pkScript []byte) (*services.TxInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TxByHash", txHash, pkScript) + ret0, _ := ret[0].(*services.TxInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TxByHash indicates an expected call of TxByHash. +func (mr *MockBtcSenderMockRecorder) TxByHash(txHash, pkScript interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TxByHash", reflect.TypeOf((*MockBtcSender)(nil).TxByHash), txHash, pkScript) +} + +// MockParamsRetriever is a mock of ParamsRetriever interface. +type MockParamsRetriever struct { + ctrl *gomock.Controller + recorder *MockParamsRetrieverMockRecorder +} + +// MockParamsRetrieverMockRecorder is the mock recorder for MockParamsRetriever. +type MockParamsRetrieverMockRecorder struct { + mock *MockParamsRetriever +} + +// NewMockParamsRetriever creates a new mock instance. +func NewMockParamsRetriever(ctrl *gomock.Controller) *MockParamsRetriever { + mock := &MockParamsRetriever{ctrl: ctrl} + mock.recorder = &MockParamsRetrieverMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockParamsRetriever) EXPECT() *MockParamsRetrieverMockRecorder { + return m.recorder +} + +// ParamsByHeight mocks base method. +func (m *MockParamsRetriever) ParamsByHeight(ctx context.Context, height uint64) (*services.SystemParams, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ParamsByHeight", ctx, height) + ret0, _ := ret[0].(*services.SystemParams) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ParamsByHeight indicates an expected call of ParamsByHeight. +func (mr *MockParamsRetrieverMockRecorder) ParamsByHeight(ctx, height interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ParamsByHeight", reflect.TypeOf((*MockParamsRetriever)(nil).ParamsByHeight), ctx, height) +} + +// MockUnbondingStore is a mock of UnbondingStore interface. +type MockUnbondingStore struct { + ctrl *gomock.Controller + recorder *MockUnbondingStoreMockRecorder +} + +// MockUnbondingStoreMockRecorder is the mock recorder for MockUnbondingStore. +type MockUnbondingStoreMockRecorder struct { + mock *MockUnbondingStore +} + +// NewMockUnbondingStore creates a new mock instance. +func NewMockUnbondingStore(ctrl *gomock.Controller) *MockUnbondingStore { + mock := &MockUnbondingStore{ctrl: ctrl} + mock.recorder = &MockUnbondingStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUnbondingStore) EXPECT() *MockUnbondingStoreMockRecorder { + return m.recorder +} + +// GetFailedUnbondingTransactions mocks base method. +func (m *MockUnbondingStore) GetFailedUnbondingTransactions(ctx context.Context) ([]*services.UnbondingTxData, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFailedUnbondingTransactions", ctx) + ret0, _ := ret[0].([]*services.UnbondingTxData) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFailedUnbondingTransactions indicates an expected call of GetFailedUnbondingTransactions. +func (mr *MockUnbondingStoreMockRecorder) GetFailedUnbondingTransactions(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFailedUnbondingTransactions", reflect.TypeOf((*MockUnbondingStore)(nil).GetFailedUnbondingTransactions), ctx) +} + +// GetNotProcessedUnbondingTransactions mocks base method. +func (m *MockUnbondingStore) GetNotProcessedUnbondingTransactions(ctx context.Context) ([]*services.UnbondingTxData, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNotProcessedUnbondingTransactions", ctx) + ret0, _ := ret[0].([]*services.UnbondingTxData) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNotProcessedUnbondingTransactions indicates an expected call of GetNotProcessedUnbondingTransactions. +func (mr *MockUnbondingStoreMockRecorder) GetNotProcessedUnbondingTransactions(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotProcessedUnbondingTransactions", reflect.TypeOf((*MockUnbondingStore)(nil).GetNotProcessedUnbondingTransactions), ctx) +} + +// GetUnbondingTransactionsWithNoQuorum mocks base method. +func (m *MockUnbondingStore) GetUnbondingTransactionsWithNoQuorum(ctx context.Context) ([]*services.UnbondingTxData, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUnbondingTransactionsWithNoQuorum", ctx) + ret0, _ := ret[0].([]*services.UnbondingTxData) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUnbondingTransactionsWithNoQuorum indicates an expected call of GetUnbondingTransactionsWithNoQuorum. +func (mr *MockUnbondingStoreMockRecorder) GetUnbondingTransactionsWithNoQuorum(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUnbondingTransactionsWithNoQuorum", reflect.TypeOf((*MockUnbondingStore)(nil).GetUnbondingTransactionsWithNoQuorum), ctx) +} + +// SetUnbondingTransactionFailedToGetCovenantSignatures mocks base method. +func (m *MockUnbondingStore) SetUnbondingTransactionFailedToGetCovenantSignatures(ctx context.Context, utx *services.UnbondingTxData) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetUnbondingTransactionFailedToGetCovenantSignatures", ctx, utx) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetUnbondingTransactionFailedToGetCovenantSignatures indicates an expected call of SetUnbondingTransactionFailedToGetCovenantSignatures. +func (mr *MockUnbondingStoreMockRecorder) SetUnbondingTransactionFailedToGetCovenantSignatures(ctx, utx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUnbondingTransactionFailedToGetCovenantSignatures", reflect.TypeOf((*MockUnbondingStore)(nil).SetUnbondingTransactionFailedToGetCovenantSignatures), ctx, utx) +} + +// SetUnbondingTransactionInputAlreadySpent mocks base method. +func (m *MockUnbondingStore) SetUnbondingTransactionInputAlreadySpent(ctx context.Context, utx *services.UnbondingTxData) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetUnbondingTransactionInputAlreadySpent", ctx, utx) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetUnbondingTransactionInputAlreadySpent indicates an expected call of SetUnbondingTransactionInputAlreadySpent. +func (mr *MockUnbondingStoreMockRecorder) SetUnbondingTransactionInputAlreadySpent(ctx, utx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUnbondingTransactionInputAlreadySpent", reflect.TypeOf((*MockUnbondingStore)(nil).SetUnbondingTransactionInputAlreadySpent), ctx, utx) +} + +// SetUnbondingTransactionProcessed mocks base method. +func (m *MockUnbondingStore) SetUnbondingTransactionProcessed(ctx context.Context, utx *services.UnbondingTxData) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetUnbondingTransactionProcessed", ctx, utx) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetUnbondingTransactionProcessed indicates an expected call of SetUnbondingTransactionProcessed. +func (mr *MockUnbondingStoreMockRecorder) SetUnbondingTransactionProcessed(ctx, utx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUnbondingTransactionProcessed", reflect.TypeOf((*MockUnbondingStore)(nil).SetUnbondingTransactionProcessed), ctx, utx) +} + +// SetUnbondingTransactionProcessingFailed mocks base method. +func (m *MockUnbondingStore) SetUnbondingTransactionProcessingFailed(ctx context.Context, utx *services.UnbondingTxData) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetUnbondingTransactionProcessingFailed", ctx, utx) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetUnbondingTransactionProcessingFailed indicates an expected call of SetUnbondingTransactionProcessingFailed. +func (mr *MockUnbondingStoreMockRecorder) SetUnbondingTransactionProcessingFailed(ctx, utx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUnbondingTransactionProcessingFailed", reflect.TypeOf((*MockUnbondingStore)(nil).SetUnbondingTransactionProcessingFailed), ctx, utx) +} diff --git a/internal/services/expected_interfaces.go b/internal/services/expected_interfaces.go index 236275b..4def1b5 100644 --- a/internal/services/expected_interfaces.go +++ b/internal/services/expected_interfaces.go @@ -18,8 +18,6 @@ type SignRequest struct { StakerUnbondingSig *schnorr.Signature // Staking output which was used to fund unbonding transaction FundingOutput *wire.TxOut - // Script of the path which should be execute - unbonding path - UnbondingScript []byte // Public key of the signer SignerPubKey *btcec.PublicKey } @@ -33,14 +31,12 @@ func NewSignRequest( tx *wire.MsgTx, stakerSig *schnorr.Signature, fundingOutput *wire.TxOut, - script []byte, pubKey *btcec.PublicKey, ) *SignRequest { return &SignRequest{ UnbondingTransaction: tx, StakerUnbondingSig: stakerSig, FundingOutput: fundingOutput, - UnbondingScript: script, SignerPubKey: pubKey, } } diff --git a/internal/services/remote_signer.go b/internal/services/remote_signer.go index 3d305be..3c3f488 100644 --- a/internal/services/remote_signer.go +++ b/internal/services/remote_signer.go @@ -41,7 +41,7 @@ func (rs *RemoteSigner) SignUnbondingTransaction(req *SignRequest) (*PubKeySigPa req.UnbondingTransaction, req.StakerUnbondingSig, req.SignerPubKey, - req.UnbondingScript, + req.FundingOutput.PkScript, ) if err != nil { diff --git a/internal/services/unbonding_pipeline.go b/internal/services/unbonding_pipeline.go index b0f4f05..56315e8 100644 --- a/internal/services/unbonding_pipeline.go +++ b/internal/services/unbonding_pipeline.go @@ -7,6 +7,7 @@ import ( "fmt" "log/slog" + "github.com/babylonchain/babylon/btcstaking" "github.com/babylonchain/babylon/types" "github.com/babylonchain/cli-tools/internal/btcclient" "github.com/babylonchain/cli-tools/internal/config" @@ -172,10 +173,13 @@ func (up *UnbondingPipeline) signUnbondingTransaction( unbondingTransaction, stakerSig, fundingOutput, - unbondingScript, pk, ) - go up.requestSigFromCovenant(req, resultChan) + go up.requestSigFromCovenant( + req, + unbondingScript, + resultChan, + ) } // check all the results @@ -200,7 +204,12 @@ func (up *UnbondingPipeline) signUnbondingTransaction( return signatures[:params.CovenantQuorum], nil } -func (up *UnbondingPipeline) requestSigFromCovenant(req *SignRequest, resultChan chan *SignResult) { +// requestSigFromCovenant sends a request to the covenant signer +// unbondingScript argument is necessary to verify the signature +func (up *UnbondingPipeline) requestSigFromCovenant( + req *SignRequest, + unbondingScript []byte, + resultChan chan *SignResult) { pkStr := pubKeyToStringCompressed(req.SignerPubKey) up.logger.Debug("request signatures from covenant signer", "signer_pk", pkStr) @@ -214,13 +223,32 @@ func (up *UnbondingPipeline) requestSigFromCovenant(req *SignRequest, resultChan "error", err) res.Err = err - } else { - up.Metrics.RecordSuccessSigningRequest(pkStr) - up.logger.Debug("got signatures from covenant signer", "signer_pk", pkStr) + resultChan <- &res + return + } - res.PubKeySig = sigPair + // verify the signature + if err := btcstaking.VerifyTransactionSigWithOutput( + req.UnbondingTransaction, + req.FundingOutput, + unbondingScript, + req.SignerPubKey, + sigPair.Signature.Serialize(), + ); err != nil { + up.Metrics.RecordFailedSigningRequest(pkStr) + up.logger.Error("failed to verify signature from covenant member", + "signer_pk", pkStr, + "error", err, + ) + res.Err = fmt.Errorf("error verify signature from covenant %s: %w", pkStr, err) + resultChan <- &res + return } + up.Metrics.RecordSuccessSigningRequest(pkStr) + up.logger.Debug("got signatures from covenant signer", "signer_pk", pkStr) + + res.PubKeySig = sigPair resultChan <- &res } @@ -326,7 +354,7 @@ func (up *UnbondingPipeline) processUnbondingTransactions( utx.UnbondingTransaction, utx.UnbondingTransactionSig, stakingOutputRecovered, - stakingOutputRecovered.PkScript, + unbondingPathSpendInfo.RevealedLeaf.Script, params, ) diff --git a/internal/services/unbonding_pipeline_test.go b/internal/services/unbonding_pipeline_test.go new file mode 100644 index 0000000..c17254b --- /dev/null +++ b/internal/services/unbonding_pipeline_test.go @@ -0,0 +1,252 @@ +package services_test + +import ( + "context" + "testing" + + "github.com/babylonchain/babylon/btcstaking" + "github.com/babylonchain/cli-tools/internal/config" + "github.com/babylonchain/cli-tools/internal/logger" + "github.com/babylonchain/cli-tools/internal/mocks" + "github.com/babylonchain/cli-tools/internal/services" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" +) + +var ( + testParams = chaincfg.MainNetParams + magicBytes = []byte{0x00, 0x01, 0x02, 0x03} +) + +type MockedDependencies struct { + bs *mocks.MockBtcSender + cs *mocks.MockCovenantSigner + st *mocks.MockUnbondingStore + pr *mocks.MockParamsRetriever +} + +func NewMockedDependencies(t *testing.T) *MockedDependencies { + ctrl := gomock.NewController(t) + return &MockedDependencies{ + bs: mocks.NewMockBtcSender(ctrl), + cs: mocks.NewMockCovenantSigner(ctrl), + st: mocks.NewMockUnbondingStore(ctrl), + pr: mocks.NewMockParamsRetriever(ctrl), + } +} + +func NewUnbondingData( + t *testing.T, + covenantInfo *CovenantInfo, + covenantQuorum uint32, +) (*services.UnbondingTxData, []*services.PubKeySigPair) { + stakingTime := uint16(1000) + stakingAmount := btcutil.Amount(100000) + unbondingFee := btcutil.Amount(1000) + unbondingTime := uint16(100) + stakerKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + stakerPubKey := stakerKey.PubKey() + fpKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + stakingInfo, stakingTx, err := btcstaking.BuildV0IdentifiableStakingOutputsAndTx( + magicBytes, + stakerPubKey, + fpKey.PubKey(), + covenantInfo.GetCovenantPublicKeys(), + covenantQuorum, + stakingTime, + stakingAmount, + &testParams, + ) + require.NoError(t, err) + + stakingUnbondingPathInfo, err := stakingInfo.UnbondingPathSpendInfo() + require.NoError(t, err) + + fakeInputHashBytes := [32]byte{} + fakeInputHash, err := chainhash.NewHash(fakeInputHashBytes[:]) + require.NoError(t, err) + fakeInputIndex := uint32(0) + stakingTx.AddTxIn(wire.NewTxIn(wire.NewOutPoint(fakeInputHash, fakeInputIndex), nil, nil)) + + unbondingInfo, err := btcstaking.BuildUnbondingInfo( + stakerPubKey, + []*btcec.PublicKey{fpKey.PubKey()}, + covenantInfo.GetCovenantPublicKeys(), + covenantQuorum, + unbondingTime, + stakingAmount-unbondingFee, + &testParams, + ) + require.NoError(t, err) + stakingTxHash := stakingTx.TxHash() + unbondingTx := wire.NewMsgTx(wire.TxVersion) + unbondingTx.AddTxIn(wire.NewTxIn(wire.NewOutPoint(&stakingTxHash, 0), nil, nil)) + unbondingTx.AddTxOut(unbondingInfo.UnbondingOutput) + + unbondingTxHash := unbondingTx.TxHash() + + validStakerSig, err := btcstaking.SignTxWithOneScriptSpendInputFromTapLeaf( + unbondingTx, + stakingInfo.StakingOutput, + stakerKey, + stakingUnbondingPathInfo.RevealedLeaf, + ) + + require.NoError(t, err) + + cov1sig, err := btcstaking.SignTxWithOneScriptSpendInputFromTapLeaf( + unbondingTx, + stakingInfo.StakingOutput, + covenantInfo.Cov1PrivKey, + stakingUnbondingPathInfo.RevealedLeaf, + ) + require.NoError(t, err) + + var sigs []*services.PubKeySigPair + sigs = append(sigs, &services.PubKeySigPair{ + Signature: cov1sig, + PubKey: covenantInfo.Cov1PrivKey.PubKey(), + }) + + utxData := &services.UnbondingTxData{ + UnbondingTransaction: unbondingTx, + UnbondingTransactionHash: &unbondingTxHash, + UnbondingTransactionSig: validStakerSig, + StakingInfo: &services.StakingInfo{ + StakerPk: stakerPubKey, + FinalityProviderPk: fpKey.PubKey(), + StakingTimelock: stakingTime, + StakingAmount: stakingAmount, + }, + StakingTransactionData: &services.StakingTransactionData{ + StakingTransaction: stakingTx, + StakingOutputIdx: 0, + }, + } + + return utxData, sigs +} + +type CovenantInfo struct { + Cov1PrivKey *btcec.PrivateKey +} + +func (c *CovenantInfo) GetCovenantPublicKeys() []*btcec.PublicKey { + return []*btcec.PublicKey{c.Cov1PrivKey.PubKey()} +} + +func NewCovenantInfo(t *testing.T) *CovenantInfo { + cov1Key, err := btcec.NewPrivateKey() + require.NoError(t, err) + + return &CovenantInfo{ + Cov1PrivKey: cov1Key, + } +} + +func TestValidSigningFlow(t *testing.T) { + log := logger.DefaultLogger() + deps := NewMockedDependencies(t) + + covenantMembers := NewCovenantInfo(t) + covenantQuorum := uint32(1) + unbondingData, covenantSignatures := NewUnbondingData(t, covenantMembers, covenantQuorum) + pipeLine := services.NewUnbondingPipeline( + log, + deps.st, + deps.cs, + deps.bs, + deps.pr, + services.NewPipelineMetrics(config.DefaultMetricsConfig()), + &testParams, + ) + require.NotNil(t, pipeLine) + + stakingTransactionHash := unbondingData.StakingTransactionData.StakingTransaction.TxHash() + deps.st.EXPECT().GetNotProcessedUnbondingTransactions(gomock.Any()).Return([]*services.UnbondingTxData{unbondingData}, nil) + stakingTxHeight := uint32(100) + deps.bs.EXPECT().TxByHash( + &stakingTransactionHash, + unbondingData.StakingOutput().PkScript, + ).Return(&services.TxInfo{ + Tx: unbondingData.StakingTransactionData.StakingTransaction, + TxInclusionHeight: stakingTxHeight, + }, nil) + deps.pr.EXPECT().ParamsByHeight(gomock.Any(), uint64(stakingTxHeight)).Return(&services.SystemParams{ + CovenantPublicKeys: covenantMembers.GetCovenantPublicKeys(), + CovenantQuorum: covenantQuorum, + MagicBytes: magicBytes, + }, nil) + deps.cs.EXPECT().SignUnbondingTransaction(gomock.Any()).Return(covenantSignatures[0], nil) + deps.bs.EXPECT().CheckTxOutSpendable( + &stakingTransactionHash, + uint32(unbondingData.StakingTransactionData.StakingOutputIdx), + true, + ).Return(true, nil) + deps.bs.EXPECT().SendTx(unbondingData.UnbondingTransaction).Return(unbondingData.UnbondingTransactionHash, nil) + deps.st.EXPECT().SetUnbondingTransactionProcessed(gomock.Any(), unbondingData).Return(nil) + + err := pipeLine.ProcessNewTransactions(context.Background()) + require.NoError(t, err) +} + +func TestInvalidSignatureHandling(t *testing.T) { + log := logger.DefaultLogger() + deps := NewMockedDependencies(t) + + covenantMembers := NewCovenantInfo(t) + covenantQuorum := uint32(1) + unbondingData, covenantSignatures := NewUnbondingData(t, covenantMembers, covenantQuorum) + pipeLine := services.NewUnbondingPipeline( + log, + deps.st, + deps.cs, + deps.bs, + deps.pr, + services.NewPipelineMetrics(config.DefaultMetricsConfig()), + &testParams, + ) + require.NotNil(t, pipeLine) + + stakingTransactionHash := unbondingData.StakingTransactionData.StakingTransaction.TxHash() + deps.st.EXPECT().GetNotProcessedUnbondingTransactions(gomock.Any()).Return([]*services.UnbondingTxData{unbondingData}, nil) + stakingTxHeight := uint32(100) + deps.bs.EXPECT().TxByHash( + &stakingTransactionHash, + unbondingData.StakingOutput().PkScript, + ).Return(&services.TxInfo{ + Tx: unbondingData.StakingTransactionData.StakingTransaction, + TxInclusionHeight: stakingTxHeight, + }, nil) + deps.pr.EXPECT().ParamsByHeight(gomock.Any(), uint64(stakingTxHeight)).Return(&services.SystemParams{ + CovenantPublicKeys: covenantMembers.GetCovenantPublicKeys(), + CovenantQuorum: covenantQuorum, + MagicBytes: magicBytes, + }, nil) + + // tamper signature so it is invalid + validSignatureKeyPair := covenantSignatures[0] + + signatureBytes := validSignatureKeyPair.Signature.Serialize() + // modify lastByte + signatureBytes[len(signatureBytes)-1] = signatureBytes[len(signatureBytes)-1] + 1 + invalidSig, err := schnorr.ParseSignature(signatureBytes) + require.NoError(t, err) + + validSignatureKeyPair.Signature = invalidSig + + deps.cs.EXPECT().SignUnbondingTransaction(gomock.Any()).Return(validSignatureKeyPair, nil) + // we receive invalid signature from the only signer, so we should fail to get quorum + deps.st.EXPECT().SetUnbondingTransactionFailedToGetCovenantSignatures(gomock.Any(), unbondingData).Return(nil) + + err = pipeLine.ProcessNewTransactions(context.Background()) + require.NoError(t, err) +}