From 97c6a5b71b71be107650d1e36924f0bed2b6e17d Mon Sep 17 00:00:00 2001 From: Branimir Tomeljak Date: Fri, 17 Jul 2026 15:45:34 +0200 Subject: [PATCH 1/6] key across signature cache by the borrow digest fields --- api/handlers/signing.go | 19 ++++++++++- api/handlers/signing_test.go | 37 +++++++++++++++++++-- chains/evm/message/across.go | 4 ++- chains/evm/signature/session.go | 18 +++++++++++ chains/evm/signature/session_test.go | 48 ++++++++++++++++++++++++++++ 5 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 chains/evm/signature/session.go create mode 100644 chains/evm/signature/session_test.go diff --git a/api/handlers/signing.go b/api/handlers/signing.go index ba617f47..4d828b8a 100644 --- a/api/handlers/signing.go +++ b/api/handlers/signing.go @@ -7,10 +7,12 @@ import ( "fmt" "math/big" "net/http" + "strconv" "github.com/ethereum/go-ethereum/common" "github.com/gorilla/mux" evmMessage "github.com/sprintertech/sprinter-signing/chains/evm/message" + "github.com/sprintertech/sprinter-signing/chains/evm/signature" lighterMessage "github.com/sprintertech/sprinter-signing/chains/lighter/message" "github.com/sygmaprotocol/sygma-core/relayer/message" ) @@ -234,7 +236,7 @@ func (h *StatusHandler) HandleRequest(w http.ResponseWriter, r *http.Request) { ctx := r.Context() sigChn := make(chan []byte, 1) - h.cache.Subscribe(ctx, fmt.Sprintf("%d-%s", chainId, depositId), sigChn) + h.cache.Subscribe(ctx, subscribeKey(r, chainId.Uint64(), depositId), sigChn) for { select { case <-r.Context().Done(): @@ -249,6 +251,21 @@ func (h *StatusHandler) HandleRequest(w http.ResponseWriter, r *http.Request) { } } +// subscribeKey uses the composite key when the caller supplies the digest fields, else the legacy key. +func subscribeKey(r *http.Request, chainID uint64, depositID string) string { + q := r.URL.Query() + deadline, errD := strconv.ParseUint(q.Get("deadline"), 10, 64) + caller := q.Get("caller") + borrowAmount, okB := new(big.Int).SetString(q.Get("borrowAmount"), 10) + liquidityPool := q.Get("liquidityPool") + repaymentChainID, errR := strconv.ParseUint(q.Get("repaymentChainId"), 10, 64) + if errD != nil || caller == "" || !okB || liquidityPool == "" || errR != nil { + return fmt.Sprintf("%d-%s", chainID, depositID) + } + return signature.BorrowSessionID(chainID, depositID, deadline, common.HexToAddress(caller), + borrowAmount, common.HexToAddress(liquidityPool), repaymentChainID) +} + func (h *StatusHandler) setheaders(w http.ResponseWriter) { w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") diff --git a/api/handlers/signing_test.go b/api/handlers/signing_test.go index 820a46a9..762f8fc9 100644 --- a/api/handlers/signing_test.go +++ b/api/handlers/signing_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/common" "github.com/gorilla/mux" "github.com/sprintertech/sprinter-signing/api/handlers" mock_handlers "github.com/sprintertech/sprinter-signing/api/handlers/mock" @@ -472,7 +473,10 @@ func (s *StatusHandlerTestSuite) Test_HandleRequest_ChainNotSupported() { } func (s *StatusHandlerTestSuite) Test_HandleRequest_ValidSignature() { - req := httptest.NewRequest(http.MethodGet, "/v1/chains/1/signatures/id", nil) + caller := "0x1111111111111111111111111111111111111111" + pool := "0x3333333333333333333333333333333333333333" + query := "?deadline=1000&caller=" + caller + "&borrowAmount=500&liquidityPool=" + pool + "&repaymentChainId=10" + req := httptest.NewRequest(http.MethodGet, "/v1/chains/1/signatures/id"+query, nil) req = mux.SetURLVars(req, map[string]string{ "chainId": "1", "depositId": "id", @@ -480,9 +484,10 @@ func (s *StatusHandlerTestSuite) Test_HandleRequest_ValidSignature() { recorder := httptest.NewRecorder() + expectedKey := "1-id-1000-" + common.HexToAddress(caller).Hex() + "-500-" + common.HexToAddress(pool).Hex() + "-10" expectedSignature := []byte{0x01, 0x02, 0x03} s.mockSignatureCacher.EXPECT(). - Subscribe(gomock.Any(), "1-id", gomock.Any()). + Subscribe(gomock.Any(), expectedKey, gomock.Any()). Do(func(ctx context.Context, id string, sigChannel chan []byte) { go func() { sigChannel <- expectedSignature @@ -491,7 +496,7 @@ func (s *StatusHandlerTestSuite) Test_HandleRequest_ValidSignature() { go s.handler.HandleRequest(recorder, req) - time.Sleep(100 * time.Millisecond) // Give some time for the goroutine to execute + time.Sleep(50 * time.Millisecond) s.Equal(http.StatusOK, recorder.Code) s.Equal("text/event-stream", recorder.Header().Get("Content-Type")) @@ -500,3 +505,29 @@ func (s *StatusHandlerTestSuite) Test_HandleRequest_ValidSignature() { s.Equal("*", recorder.Header().Get("Access-Control-Allow-Origin")) s.Equal("data: "+hex.EncodeToString(expectedSignature)+"\n\n", recorder.Body.String()) } + +func (s *StatusHandlerTestSuite) Test_HandleRequest_LegacyKeyWithoutParams() { + req := httptest.NewRequest(http.MethodGet, "/v1/chains/1/signatures/id", nil) + req = mux.SetURLVars(req, map[string]string{ + "chainId": "1", + "depositId": "id", + }) + + recorder := httptest.NewRecorder() + + expectedSignature := []byte{0x01, 0x02, 0x03} + s.mockSignatureCacher.EXPECT(). + Subscribe(gomock.Any(), "1-id", gomock.Any()). + Do(func(ctx context.Context, id string, sigChannel chan []byte) { + go func() { + sigChannel <- expectedSignature + }() + }) + + go s.handler.HandleRequest(recorder, req) + + time.Sleep(100 * time.Millisecond) // Give some time for the goroutine to execute + + s.Equal(http.StatusOK, recorder.Code) + s.Equal("data: "+hex.EncodeToString(expectedSignature)+"\n\n", recorder.Body.String()) +} diff --git a/chains/evm/message/across.go b/chains/evm/message/across.go index 806818eb..d98a6f20 100644 --- a/chains/evm/message/across.go +++ b/chains/evm/message/across.go @@ -197,7 +197,9 @@ func (h *AcrossMessageHandler) HandleMessage(m *message.Message) (*proposal.Prop return nil, err } - sessionID := fmt.Sprintf("%d-%s", sourceChainID, data.DepositId) + sessionID := signature.BorrowSessionID( + sourceChainID, data.DepositId.String(), data.Deadline, data.Caller, + data.BorrowAmount, data.LiquidityPool, data.RepaymentChainID) signing, err := signing.NewSigning( new(big.Int).SetBytes(unlockHash), sessionID, diff --git a/chains/evm/signature/session.go b/chains/evm/signature/session.go new file mode 100644 index 00000000..3f9f8268 --- /dev/null +++ b/chains/evm/signature/session.go @@ -0,0 +1,18 @@ +package signature + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +// BorrowSessionID keys the signing session and cache by the digest fields not already fixed by the deposit. +func BorrowSessionID( + chainID uint64, depositID string, deadline uint64, caller common.Address, + borrowAmount *big.Int, liquidityPool common.Address, repaymentChainID uint64, +) string { + return fmt.Sprintf("%d-%s-%d-%s-%s-%s-%d", + chainID, depositID, deadline, caller.Hex(), + borrowAmount.String(), liquidityPool.Hex(), repaymentChainID) +} diff --git a/chains/evm/signature/session_test.go b/chains/evm/signature/session_test.go new file mode 100644 index 00000000..4032b88c --- /dev/null +++ b/chains/evm/signature/session_test.go @@ -0,0 +1,48 @@ +package signature_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/sprintertech/sprinter-signing/chains/evm/signature" +) + +func TestBorrowSessionID(t *testing.T) { + caller := common.HexToAddress("0x1111111111111111111111111111111111111111") + other := common.HexToAddress("0x2222222222222222222222222222222222222222") + pool := common.HexToAddress("0x3333333333333333333333333333333333333333") + otherPool := common.HexToAddress("0x4444444444444444444444444444444444444444") + id := func(chainID uint64, deposit string, deadline uint64, c common.Address, + amount *big.Int, lp common.Address, repay uint64) string { + return signature.BorrowSessionID(chainID, deposit, deadline, c, amount, lp, repay) + } + base := id(1, "42", 1000, caller, big.NewInt(500), pool, 10) + + tests := []struct { + name string + got string + equal bool + }{ + {"identical inputs match", id(1, "42", 1000, caller, big.NewInt(500), pool, 10), true}, + {"different chain differs", id(2, "42", 1000, caller, big.NewInt(500), pool, 10), false}, + {"different deposit differs", id(1, "43", 1000, caller, big.NewInt(500), pool, 10), false}, + {"different deadline differs", id(1, "42", 1001, caller, big.NewInt(500), pool, 10), false}, + {"different caller differs", id(1, "42", 1000, other, big.NewInt(500), pool, 10), false}, + {"different borrow amount differs", id(1, "42", 1000, caller, big.NewInt(501), pool, 10), false}, + {"different liquidity pool differs", id(1, "42", 1000, caller, big.NewInt(500), otherPool, 10), false}, + {"different repayment chain differs", id(1, "42", 1000, caller, big.NewInt(500), pool, 11), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if (tt.got == base) != tt.equal { + t.Fatalf("equal=%v, base=%q got=%q", tt.equal, base, tt.got) + } + }) + } + + want := "1-42-1000-" + caller.Hex() + "-500-" + pool.Hex() + "-10" + if base != want { + t.Fatalf("format: want %q got %q", want, base) + } +} From 58f0257b391102ee80c99b22788561e2f8ec8bb2 Mon Sep 17 00:00:00 2001 From: Branimir Tomeljak Date: Mon, 20 Jul 2026 12:58:58 +0200 Subject: [PATCH 2/6] optimizations --- api/handlers/signing.go | 28 +++++- api/handlers/signing_test.go | 190 ++++++++++++++++++----------------- 2 files changed, 119 insertions(+), 99 deletions(-) diff --git a/api/handlers/signing.go b/api/handlers/signing.go index 4d828b8a..a48972e5 100644 --- a/api/handlers/signing.go +++ b/api/handlers/signing.go @@ -230,13 +230,19 @@ func (h *StatusHandler) HandleRequest(w http.ResponseWriter, r *http.Request) { return } + key, err := subscribeKey(r, chainId.Uint64(), depositId) + if err != nil { + JSONError(w, err, http.StatusBadRequest) + return + } + h.setheaders(w) w.WriteHeader(http.StatusOK) w.(http.Flusher).Flush() ctx := r.Context() sigChn := make(chan []byte, 1) - h.cache.Subscribe(ctx, subscribeKey(r, chainId.Uint64(), depositId), sigChn) + h.cache.Subscribe(ctx, key, sigChn) for { select { case <-r.Context().Done(): @@ -251,19 +257,31 @@ func (h *StatusHandler) HandleRequest(w http.ResponseWriter, r *http.Request) { } } -// subscribeKey uses the composite key when the caller supplies the digest fields, else the legacy key. -func subscribeKey(r *http.Request, chainID uint64, depositID string) string { +// subscribeKey builds the cache key the status stream subscribes to, which must match the publish-side key. +func subscribeKey(r *http.Request, chainID uint64, depositID string) (string, error) { q := r.URL.Query() + // No digest fields supplied: legacy key used by protocols other than Across. + if !q.Has("deadline") && !q.Has("caller") && !q.Has("borrowAmount") && + !q.Has("liquidityPool") && !q.Has("repaymentChainId") { + return fmt.Sprintf("%d-%s", chainID, depositID), nil + } + deadline, errD := strconv.ParseUint(q.Get("deadline"), 10, 64) caller := q.Get("caller") borrowAmount, okB := new(big.Int).SetString(q.Get("borrowAmount"), 10) liquidityPool := q.Get("liquidityPool") repaymentChainID, errR := strconv.ParseUint(q.Get("repaymentChainId"), 10, 64) if errD != nil || caller == "" || !okB || liquidityPool == "" || errR != nil { - return fmt.Sprintf("%d-%s", chainID, depositID) + return "", fmt.Errorf("incomplete borrow parameters for composite signature key") + } + + // Normalize like the publish side (big.Int) so a non-canonical decimal cannot desync the keys. + if id, ok := new(big.Int).SetString(depositID, 10); ok { + depositID = id.String() } + // Composite Across key with all fields required, so a partial set errors above rather than silently mismatching. return signature.BorrowSessionID(chainID, depositID, deadline, common.HexToAddress(caller), - borrowAmount, common.HexToAddress(liquidityPool), repaymentChainID) + borrowAmount, common.HexToAddress(liquidityPool), repaymentChainID), nil } func (h *StatusHandler) setheaders(w http.ResponseWriter) { diff --git a/api/handlers/signing_test.go b/api/handlers/signing_test.go index 762f8fc9..2bb3c41a 100644 --- a/api/handlers/signing_test.go +++ b/api/handlers/signing_test.go @@ -17,6 +17,7 @@ import ( "github.com/sprintertech/sprinter-signing/api/handlers" mock_handlers "github.com/sprintertech/sprinter-signing/api/handlers/mock" across "github.com/sprintertech/sprinter-signing/chains/evm/message" + "github.com/sprintertech/sprinter-signing/chains/evm/signature" lighter "github.com/sprintertech/sprinter-signing/chains/lighter/message" "github.com/stretchr/testify/suite" "github.com/sygmaprotocol/sygma-core/relayer/message" @@ -430,104 +431,105 @@ func (s *StatusHandlerTestSuite) SetupTest() { s.handler = handlers.NewStatusHandler(s.mockSignatureCacher, chains) } -func (s *StatusHandlerTestSuite) Test_HandleRequest_MissingDepositID() { - req := httptest.NewRequest(http.MethodGet, "/v1/chains/1/signatures", nil) - req.Header.Set("Content-Type", "application/json") - req = mux.SetURLVars(req, map[string]string{ - "chainId": "1", - }) - - recorder := httptest.NewRecorder() - - s.handler.HandleRequest(recorder, req) - - s.Equal(http.StatusBadRequest, recorder.Code) -} - -func (s *StatusHandlerTestSuite) Test_HandleRequest_InvalidChainID() { - req := httptest.NewRequest(http.MethodGet, "/v1/chains/invalid/signatures", nil) - req = mux.SetURLVars(req, map[string]string{ - "chainId": "invalid", - "depositId": "id", - }) - - recorder := httptest.NewRecorder() - - s.handler.HandleRequest(recorder, req) - - s.Equal(http.StatusBadRequest, recorder.Code) -} - -func (s *StatusHandlerTestSuite) Test_HandleRequest_ChainNotSupported() { - req := httptest.NewRequest(http.MethodGet, "/v1/chains/3/signatures", nil) - req = mux.SetURLVars(req, map[string]string{ - "chainId": "3", - "depositId": "id", - }) - - recorder := httptest.NewRecorder() - - s.handler.HandleRequest(recorder, req) - - s.Equal(http.StatusNotFound, recorder.Code) -} - -func (s *StatusHandlerTestSuite) Test_HandleRequest_ValidSignature() { - caller := "0x1111111111111111111111111111111111111111" - pool := "0x3333333333333333333333333333333333333333" - query := "?deadline=1000&caller=" + caller + "&borrowAmount=500&liquidityPool=" + pool + "&repaymentChainId=10" - req := httptest.NewRequest(http.MethodGet, "/v1/chains/1/signatures/id"+query, nil) - req = mux.SetURLVars(req, map[string]string{ - "chainId": "1", - "depositId": "id", - }) +func (s *StatusHandlerTestSuite) Test_HandleRequest_Errors() { + tests := []struct { + name string + vars map[string]string + query string + wantCode int + }{ + { + name: "missing deposit id", + vars: map[string]string{"chainId": "1"}, + wantCode: http.StatusBadRequest, + }, + { + name: "invalid chain id", + vars: map[string]string{"chainId": "invalid", "depositId": "id"}, + wantCode: http.StatusBadRequest, + }, + { + name: "chain not supported", + vars: map[string]string{"chainId": "3", "depositId": "id"}, + wantCode: http.StatusNotFound, + }, + { + // A partial composite param set must fail fast, not fall back to the legacy key and hang. + name: "partial composite params", + vars: map[string]string{"chainId": "1", "depositId": "id"}, + query: "?deadline=1000&caller=0x1111111111111111111111111111111111111111", + wantCode: http.StatusBadRequest, + }, + } + for _, tt := range tests { + s.Run(tt.name, func() { + req := httptest.NewRequest(http.MethodGet, "/v1/chains/1/signatures/id"+tt.query, nil) + req = mux.SetURLVars(req, tt.vars) + recorder := httptest.NewRecorder() - recorder := httptest.NewRecorder() + s.handler.HandleRequest(recorder, req) - expectedKey := "1-id-1000-" + common.HexToAddress(caller).Hex() + "-500-" + common.HexToAddress(pool).Hex() + "-10" - expectedSignature := []byte{0x01, 0x02, 0x03} - s.mockSignatureCacher.EXPECT(). - Subscribe(gomock.Any(), expectedKey, gomock.Any()). - Do(func(ctx context.Context, id string, sigChannel chan []byte) { - go func() { - sigChannel <- expectedSignature - }() + s.Equal(tt.wantCode, recorder.Code) }) - - go s.handler.HandleRequest(recorder, req) - - time.Sleep(50 * time.Millisecond) - - s.Equal(http.StatusOK, recorder.Code) - s.Equal("text/event-stream", recorder.Header().Get("Content-Type")) - s.Equal("no-cache", recorder.Header().Get("Cache-Control")) - s.Equal("keep-alive", recorder.Header().Get("Connection")) - s.Equal("*", recorder.Header().Get("Access-Control-Allow-Origin")) - s.Equal("data: "+hex.EncodeToString(expectedSignature)+"\n\n", recorder.Body.String()) + } } -func (s *StatusHandlerTestSuite) Test_HandleRequest_LegacyKeyWithoutParams() { - req := httptest.NewRequest(http.MethodGet, "/v1/chains/1/signatures/id", nil) - req = mux.SetURLVars(req, map[string]string{ - "chainId": "1", - "depositId": "id", - }) - - recorder := httptest.NewRecorder() - - expectedSignature := []byte{0x01, 0x02, 0x03} - s.mockSignatureCacher.EXPECT(). - Subscribe(gomock.Any(), "1-id", gomock.Any()). - Do(func(ctx context.Context, id string, sigChannel chan []byte) { - go func() { - sigChannel <- expectedSignature - }() +func (s *StatusHandlerTestSuite) Test_HandleRequest_SubscribeKey() { + caller := common.HexToAddress("0x1111111111111111111111111111111111111111") + pool := common.HexToAddress("0x3333333333333333333333333333333333333333") + lowerCaller := common.HexToAddress("0xabcdef0123456789abcdef0123456789abcdef01") + lowerPool := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + + tests := []struct { + name string + depositId string + query string + wantKey string + }{ + { + name: "legacy key without params", + depositId: "id", + wantKey: "1-id", + }, + { + name: "composite key from params", + depositId: "id", + query: "?deadline=1000&caller=" + caller.Hex() + "&borrowAmount=500&liquidityPool=" + pool.Hex() + "&repaymentChainId=10", + wantKey: signature.BorrowSessionID(1, "id", 1000, caller, big.NewInt(500), pool, 10), + }, + { + // Lowercase addresses and a leading-zero deposit id must still map to the canonical publish key. + name: "composite key normalizes address casing and deposit id", + depositId: "0042", + query: "?deadline=1000&caller=0xabcdef0123456789abcdef0123456789abcdef01&borrowAmount=500&liquidityPool=0x1234567890abcdef1234567890abcdef12345678&repaymentChainId=10", + wantKey: signature.BorrowSessionID(1, "42", 1000, lowerCaller, big.NewInt(500), lowerPool, 10), + }, + } + for _, tt := range tests { + s.Run(tt.name, func() { + req := httptest.NewRequest(http.MethodGet, "/v1/chains/1/signatures/"+tt.depositId+tt.query, nil) + req = mux.SetURLVars(req, map[string]string{"chainId": "1", "depositId": tt.depositId}) + recorder := httptest.NewRecorder() + + expectedSignature := []byte{0x01, 0x02, 0x03} + s.mockSignatureCacher.EXPECT(). + Subscribe(gomock.Any(), tt.wantKey, gomock.Any()). + Do(func(ctx context.Context, id string, sigChannel chan []byte) { + go func() { + sigChannel <- expectedSignature + }() + }) + + go s.handler.HandleRequest(recorder, req) + + time.Sleep(50 * time.Millisecond) + + s.Equal(http.StatusOK, recorder.Code) + s.Equal("text/event-stream", recorder.Header().Get("Content-Type")) + s.Equal("no-cache", recorder.Header().Get("Cache-Control")) + s.Equal("keep-alive", recorder.Header().Get("Connection")) + s.Equal("*", recorder.Header().Get("Access-Control-Allow-Origin")) + s.Equal("data: "+hex.EncodeToString(expectedSignature)+"\n\n", recorder.Body.String()) }) - - go s.handler.HandleRequest(recorder, req) - - time.Sleep(100 * time.Millisecond) // Give some time for the goroutine to execute - - s.Equal(http.StatusOK, recorder.Code) - s.Equal("data: "+hex.EncodeToString(expectedSignature)+"\n\n", recorder.Body.String()) + } } From b19f8977877d22ddcff801a55580720b1a5dd076 Mon Sep 17 00:00:00 2001 From: Branimir Tomeljak Date: Mon, 20 Jul 2026 13:00:30 +0200 Subject: [PATCH 3/6] comment --- api/handlers/signing.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/handlers/signing.go b/api/handlers/signing.go index a48972e5..20b7e7d4 100644 --- a/api/handlers/signing.go +++ b/api/handlers/signing.go @@ -257,10 +257,10 @@ func (h *StatusHandler) HandleRequest(w http.ResponseWriter, r *http.Request) { } } -// subscribeKey builds the cache key the status stream subscribes to, which must match the publish-side key. +// subscribeKey builds the cache key the status stream subscribes to. func subscribeKey(r *http.Request, chainID uint64, depositID string) (string, error) { q := r.URL.Query() - // No digest fields supplied: legacy key used by protocols other than Across. + // No digest fields supplied: key used by protocols other than Across. if !q.Has("deadline") && !q.Has("caller") && !q.Has("borrowAmount") && !q.Has("liquidityPool") && !q.Has("repaymentChainId") { return fmt.Sprintf("%d-%s", chainID, depositID), nil @@ -279,7 +279,7 @@ func subscribeKey(r *http.Request, chainID uint64, depositID string) (string, er if id, ok := new(big.Int).SetString(depositID, 10); ok { depositID = id.String() } - // Composite Across key with all fields required, so a partial set errors above rather than silently mismatching. + return signature.BorrowSessionID(chainID, depositID, deadline, common.HexToAddress(caller), borrowAmount, common.HexToAddress(liquidityPool), repaymentChainID), nil } From 842ae89250241992f6f350a582125f57d353847b Mon Sep 17 00:00:00 2001 From: Branimir Tomeljak Date: Mon, 20 Jul 2026 15:37:45 +0200 Subject: [PATCH 4/6] invalidate cached signature on new signing request --- api/handlers/mock/signing.go | 36 +++ api/handlers/signing.go | 65 ++-- api/handlers/signing_test.go | 458 +++++++++++---------------- app/app.go | 2 +- cache/signature.go | 5 + cache/signature_test.go | 18 ++ chains/evm/message/across.go | 4 +- chains/evm/signature/session.go | 18 -- chains/evm/signature/session_test.go | 48 --- 9 files changed, 278 insertions(+), 376 deletions(-) delete mode 100644 chains/evm/signature/session.go delete mode 100644 chains/evm/signature/session_test.go diff --git a/api/handlers/mock/signing.go b/api/handlers/mock/signing.go index b3796e39..8afbba5d 100644 --- a/api/handlers/mock/signing.go +++ b/api/handlers/mock/signing.go @@ -51,3 +51,39 @@ func (mr *MockSignatureCacherMockRecorder) Subscribe(ctx, id, sigChannel any) *g mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscribe", reflect.TypeOf((*MockSignatureCacher)(nil).Subscribe), ctx, id, sigChannel) } + +// MockSignatureRemover is a mock of SignatureRemover interface. +type MockSignatureRemover struct { + ctrl *gomock.Controller + recorder *MockSignatureRemoverMockRecorder + isgomock struct{} +} + +// MockSignatureRemoverMockRecorder is the mock recorder for MockSignatureRemover. +type MockSignatureRemoverMockRecorder struct { + mock *MockSignatureRemover +} + +// NewMockSignatureRemover creates a new mock instance. +func NewMockSignatureRemover(ctrl *gomock.Controller) *MockSignatureRemover { + mock := &MockSignatureRemover{ctrl: ctrl} + mock.recorder = &MockSignatureRemoverMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSignatureRemover) EXPECT() *MockSignatureRemoverMockRecorder { + return m.recorder +} + +// Remove mocks base method. +func (m *MockSignatureRemover) Remove(id string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Remove", id) +} + +// Remove indicates an expected call of Remove. +func (mr *MockSignatureRemoverMockRecorder) Remove(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockSignatureRemover)(nil).Remove), id) +} diff --git a/api/handlers/signing.go b/api/handlers/signing.go index 20b7e7d4..b45aef2b 100644 --- a/api/handlers/signing.go +++ b/api/handlers/signing.go @@ -7,12 +7,11 @@ import ( "fmt" "math/big" "net/http" - "strconv" "github.com/ethereum/go-ethereum/common" "github.com/gorilla/mux" evmMessage "github.com/sprintertech/sprinter-signing/chains/evm/message" - "github.com/sprintertech/sprinter-signing/chains/evm/signature" + lighterChain "github.com/sprintertech/sprinter-signing/chains/lighter" lighterMessage "github.com/sprintertech/sprinter-signing/chains/lighter/message" "github.com/sygmaprotocol/sygma-core/relayer/message" ) @@ -43,14 +42,16 @@ type SigningBody struct { } type SigningHandler struct { - msgChan chan []*message.Message - chains map[uint64]struct{} + msgChan chan []*message.Message + chains map[uint64]struct{} + sigCache SignatureRemover } -func NewSigningHandler(msgChan chan []*message.Message, chains map[uint64]struct{}) *SigningHandler { +func NewSigningHandler(msgChan chan []*message.Message, chains map[uint64]struct{}, sigCache SignatureRemover) *SigningHandler { return &SigningHandler{ - msgChan: msgChan, - chains: chains, + msgChan: msgChan, + chains: chains, + sigCache: sigCache, } } @@ -144,6 +145,8 @@ func (h *SigningHandler) HandleSigning(w http.ResponseWriter, r *http.Request) { JSONError(w, fmt.Errorf("invalid protocol %s", b.Protocol), http.StatusBadRequest) return } + // a new signing request supersedes any cached signature for this deposit + h.sigCache.Remove(sessionKey(b.Protocol, b.ChainId, b.DepositId)) h.msgChan <- []*message.Message{m} err = <-errChn @@ -194,10 +197,23 @@ func (h *SigningHandler) validate(b *SigningBody, vars map[string]string) error return nil } +// sessionKey mirrors the signing session id each protocol handler builds for the cache. +func sessionKey(protocol ProtocolType, chainID uint64, depositID string) string { + // lighter signs under its fixed domain id, not the request chain id + if protocol == LighterProtocol { + return fmt.Sprintf("%d-%s", lighterChain.LIGHTER_DOMAIN_ID, depositID) + } + return fmt.Sprintf("%d-%s", chainID, depositID) +} + type SignatureCacher interface { Subscribe(ctx context.Context, id string, sigChannel chan []byte) } +type SignatureRemover interface { + Remove(id string) +} + type StatusHandler struct { cache SignatureCacher chains map[uint64]struct{} @@ -230,19 +246,13 @@ func (h *StatusHandler) HandleRequest(w http.ResponseWriter, r *http.Request) { return } - key, err := subscribeKey(r, chainId.Uint64(), depositId) - if err != nil { - JSONError(w, err, http.StatusBadRequest) - return - } - h.setheaders(w) w.WriteHeader(http.StatusOK) w.(http.Flusher).Flush() ctx := r.Context() sigChn := make(chan []byte, 1) - h.cache.Subscribe(ctx, key, sigChn) + h.cache.Subscribe(ctx, fmt.Sprintf("%d-%s", chainId, depositId), sigChn) for { select { case <-r.Context().Done(): @@ -257,33 +267,6 @@ func (h *StatusHandler) HandleRequest(w http.ResponseWriter, r *http.Request) { } } -// subscribeKey builds the cache key the status stream subscribes to. -func subscribeKey(r *http.Request, chainID uint64, depositID string) (string, error) { - q := r.URL.Query() - // No digest fields supplied: key used by protocols other than Across. - if !q.Has("deadline") && !q.Has("caller") && !q.Has("borrowAmount") && - !q.Has("liquidityPool") && !q.Has("repaymentChainId") { - return fmt.Sprintf("%d-%s", chainID, depositID), nil - } - - deadline, errD := strconv.ParseUint(q.Get("deadline"), 10, 64) - caller := q.Get("caller") - borrowAmount, okB := new(big.Int).SetString(q.Get("borrowAmount"), 10) - liquidityPool := q.Get("liquidityPool") - repaymentChainID, errR := strconv.ParseUint(q.Get("repaymentChainId"), 10, 64) - if errD != nil || caller == "" || !okB || liquidityPool == "" || errR != nil { - return "", fmt.Errorf("incomplete borrow parameters for composite signature key") - } - - // Normalize like the publish side (big.Int) so a non-canonical decimal cannot desync the keys. - if id, ok := new(big.Int).SetString(depositID, 10); ok { - depositID = id.String() - } - - return signature.BorrowSessionID(chainID, depositID, deadline, common.HexToAddress(caller), - borrowAmount, common.HexToAddress(liquidityPool), repaymentChainID), nil -} - func (h *StatusHandler) setheaders(w http.ResponseWriter) { w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") diff --git a/api/handlers/signing_test.go b/api/handlers/signing_test.go index 2bb3c41a..fd356c0f 100644 --- a/api/handlers/signing_test.go +++ b/api/handlers/signing_test.go @@ -12,12 +12,11 @@ import ( "testing" "time" - "github.com/ethereum/go-ethereum/common" "github.com/gorilla/mux" "github.com/sprintertech/sprinter-signing/api/handlers" mock_handlers "github.com/sprintertech/sprinter-signing/api/handlers/mock" across "github.com/sprintertech/sprinter-signing/chains/evm/message" - "github.com/sprintertech/sprinter-signing/chains/evm/signature" + lighterChain "github.com/sprintertech/sprinter-signing/chains/lighter" lighter "github.com/sprintertech/sprinter-signing/chains/lighter/message" "github.com/stretchr/testify/suite" "github.com/sygmaprotocol/sygma-core/relayer/message" @@ -27,7 +26,8 @@ import ( type SigningHandlerTestSuite struct { suite.Suite - chains map[uint64]struct{} + chains map[uint64]struct{} + mockRemover *mock_handlers.MockSignatureRemover } func TestRunSigningHandlerTestSuite(t *testing.T) { @@ -35,14 +35,16 @@ func TestRunSigningHandlerTestSuite(t *testing.T) { } func (s *SigningHandlerTestSuite) SetupTest() { + ctrl := gomock.NewController(s.T()) chains := make(map[uint64]struct{}) chains[1] = struct{}{} s.chains = chains + s.mockRemover = mock_handlers.NewMockSignatureRemover(ctrl) } func (s *SigningHandlerTestSuite) Test_HandleSigning_MissingDepositID() { msgChn := make(chan []*message.Message) - handler := handlers.NewSigningHandler(msgChn, s.chains) + handler := handlers.NewSigningHandler(msgChn, s.chains, s.mockRemover) input := handlers.SigningBody{ Protocol: "across", @@ -73,7 +75,7 @@ func (s *SigningHandlerTestSuite) Test_HandleSigning_MissingDepositID() { func (s *SigningHandlerTestSuite) Test_HandleSigning_MissingCaller() { msgChn := make(chan []*message.Message) - handler := handlers.NewSigningHandler(msgChn, s.chains) + handler := handlers.NewSigningHandler(msgChn, s.chains, s.mockRemover) input := handlers.SigningBody{ Protocol: "across", @@ -104,7 +106,7 @@ func (s *SigningHandlerTestSuite) Test_HandleSigning_MissingCaller() { func (s *SigningHandlerTestSuite) Test_HandleSigning_MissingLiquidityPool() { msgChn := make(chan []*message.Message) - handler := handlers.NewSigningHandler(msgChn, s.chains) + handler := handlers.NewSigningHandler(msgChn, s.chains, s.mockRemover) input := handlers.SigningBody{ Protocol: "across", @@ -135,7 +137,7 @@ func (s *SigningHandlerTestSuite) Test_HandleSigning_MissingLiquidityPool() { func (s *SigningHandlerTestSuite) Test_HandleSigning_InvalidChainID() { msgChn := make(chan []*message.Message) - handler := handlers.NewSigningHandler(msgChn, s.chains) + handler := handlers.NewSigningHandler(msgChn, s.chains, s.mockRemover) input := handlers.SigningBody{ DepositId: "1000", @@ -167,7 +169,7 @@ func (s *SigningHandlerTestSuite) Test_HandleSigning_InvalidChainID() { func (s *SigningHandlerTestSuite) Test_HandleSigning_ChainNotSupported() { msgChn := make(chan []*message.Message) - handler := handlers.NewSigningHandler(msgChn, s.chains) + handler := handlers.NewSigningHandler(msgChn, s.chains, s.mockRemover) input := handlers.SigningBody{ DepositId: "1000", @@ -199,7 +201,7 @@ func (s *SigningHandlerTestSuite) Test_HandleSigning_ChainNotSupported() { func (s *SigningHandlerTestSuite) Test_HandleSigning_InvalidProtocol() { msgChn := make(chan []*message.Message) - handler := handlers.NewSigningHandler(msgChn, s.chains) + handler := handlers.NewSigningHandler(msgChn, s.chains, s.mockRemover) input := handlers.SigningBody{ DepositId: "1000", @@ -229,307 +231,233 @@ func (s *SigningHandlerTestSuite) Test_HandleSigning_InvalidProtocol() { s.Equal(http.StatusBadRequest, recorder.Code) } -func (s *SigningHandlerTestSuite) Test_HandleSigning_ErrorHandlingMessage() { - msgChn := make(chan []*message.Message) - handler := handlers.NewSigningHandler(msgChn, s.chains) - - input := handlers.SigningBody{ - DepositId: "1000", - Protocol: "across", - LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - Nonce: &handlers.BigInt{big.NewInt(1001)}, - BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, - //nolint:gosec - Deadline: uint64(time.Now().Unix()), +func (s *SigningHandlerTestSuite) Test_HandleSigning_DispatchesAndInvalidates() { + tests := []struct { + name string + body handlers.SigningBody + wantKey string + respond func(msg []*message.Message) + wantCode int + }{ + { + name: "across signing error", + body: handlers.SigningBody{ + DepositId: "1000", + Protocol: "across", + LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Nonce: &handlers.BigInt{big.NewInt(1001)}, + BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, + //nolint:gosec + Deadline: uint64(time.Now().Unix()), + }, + wantKey: "1-1000", + respond: func(msg []*message.Message) { + ad := msg[0].Data.(*across.AcrossData) + ad.ErrChn <- fmt.Errorf("error handling message") + }, + wantCode: http.StatusInternalServerError, + }, + { + name: "across success", + body: handlers.SigningBody{ + DepositId: "1000", + Protocol: "across", + LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, + Nonce: &handlers.BigInt{big.NewInt(1001)}, + RepaymentChainId: 5, + //nolint:gosec + Deadline: uint64(time.Now().Unix()), + }, + wantKey: "1-1000", + respond: func(msg []*message.Message) { + ad := msg[0].Data.(*across.AcrossData) + s.Equal(ad.RepaymentChainID, uint64(5)) + ad.ErrChn <- nil + }, + wantCode: http.StatusAccepted, + }, + { + name: "lifi-escrow success", + body: handlers.SigningBody{ + DepositId: "depositID", + Protocol: "lifi-escrow", + LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Calldata: "0xbe5", + Nonce: &handlers.BigInt{big.NewInt(1001)}, + BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, + //nolint:gosec + Deadline: uint64(time.Now().Unix()), + }, + wantKey: "1-depositID", + respond: func(msg []*message.Message) { + ad := msg[0].Data.(*across.LifiEscrowData) + ad.ErrChn <- nil + }, + wantCode: http.StatusAccepted, + }, + { + name: "lighter success", + body: handlers.SigningBody{ + DepositId: "depositID", + Protocol: "lighter", + LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Calldata: "0xbe5", + Nonce: &handlers.BigInt{big.NewInt(1001)}, + BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, + //nolint:gosec + Deadline: uint64(time.Now().Unix()), + }, + wantKey: fmt.Sprintf("%d-depositID", lighterChain.LIGHTER_DOMAIN_ID), + respond: func(msg []*message.Message) { + ad := msg[0].Data.(*lighter.LighterData) + ad.ErrChn <- nil + }, + wantCode: http.StatusAccepted, + }, + { + name: "sprinter-credit success", + body: handlers.SigningBody{ + DepositId: "depositID", + Protocol: "sprinter-credit", + LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Calldata: "0xbe5", + Nonce: &handlers.BigInt{big.NewInt(1001)}, + BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, + //nolint:gosec + Deadline: uint64(time.Now().Unix()), + }, + wantKey: "1-depositID", + respond: func(msg []*message.Message) { + ad := msg[0].Data.(*across.SprinterCreditData) + ad.ErrChn <- nil + }, + wantCode: http.StatusAccepted, + }, } - body, _ := json.Marshal(input) - req := httptest.NewRequest(http.MethodPost, "/v1/chains/1/signatures", bytes.NewReader(body)) - req = mux.SetURLVars(req, map[string]string{ - "chainId": "1", - }) - req.Header.Set("Content-Type", "application/json") + for _, tt := range tests { + s.Run(tt.name, func() { + msgChn := make(chan []*message.Message) + handler := handlers.NewSigningHandler(msgChn, s.chains, s.mockRemover) - recorder := httptest.NewRecorder() + body, _ := json.Marshal(tt.body) + req := httptest.NewRequest(http.MethodPost, "/v1/chains/1/signatures", bytes.NewReader(body)) + req = mux.SetURLVars(req, map[string]string{ + "chainId": "1", + }) + req.Header.Set("Content-Type", "application/json") - go func() { - msg := <-msgChn - ad := msg[0].Data.(*across.AcrossData) - ad.ErrChn <- fmt.Errorf("error handling message") - }() + recorder := httptest.NewRecorder() - handler.HandleSigning(recorder, req) + go func() { + tt.respond(<-msgChn) + }() - s.Equal(http.StatusInternalServerError, recorder.Code) -} + s.mockRemover.EXPECT().Remove(tt.wantKey) -func (s *SigningHandlerTestSuite) Test_HandleSigning_AcrossSuccess() { - msgChn := make(chan []*message.Message) - handler := handlers.NewSigningHandler(msgChn, s.chains) + handler.HandleSigning(recorder, req) - input := handlers.SigningBody{ - DepositId: "1000", - Protocol: "across", - LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, - Nonce: &handlers.BigInt{big.NewInt(1001)}, - RepaymentChainId: 5, - //nolint:gosec - Deadline: uint64(time.Now().Unix()), + s.Equal(tt.wantCode, recorder.Code) + }) } - body, _ := json.Marshal(input) - - req := httptest.NewRequest(http.MethodPost, "/v1/chains/1/signatures", bytes.NewReader(body)) - req = mux.SetURLVars(req, map[string]string{ - "chainId": "1", - }) - req.Header.Set("Content-Type", "application/json") - - recorder := httptest.NewRecorder() +} - go func() { - msg := <-msgChn - ad := msg[0].Data.(*across.AcrossData) - s.Equal(ad.RepaymentChainID, uint64(5)) - ad.ErrChn <- nil - }() +type StatusHandlerTestSuite struct { + suite.Suite - handler.HandleSigning(recorder, req) + mockSignatureCacher *mock_handlers.MockSignatureCacher + handler *handlers.StatusHandler +} - s.Equal(http.StatusAccepted, recorder.Code) +func TestRunStatusHandlerTestSuite(t *testing.T) { + suite.Run(t, new(StatusHandlerTestSuite)) } -func (s *SigningHandlerTestSuite) Test_HandleSigning_LifiSuccess() { - msgChn := make(chan []*message.Message) - handler := handlers.NewSigningHandler(msgChn, s.chains) +func (s *StatusHandlerTestSuite) SetupTest() { + ctrl := gomock.NewController(s.T()) + defer ctrl.Finish() - input := handlers.SigningBody{ - DepositId: "depositID", - Protocol: "lifi-escrow", - LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - Calldata: "0xbe5", - Nonce: &handlers.BigInt{big.NewInt(1001)}, - BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, - //nolint:gosec - Deadline: uint64(time.Now().Unix()), - } - body, _ := json.Marshal(input) + chains := make(map[uint64]struct{}) + chains[1] = struct{}{} - req := httptest.NewRequest(http.MethodPost, "/v1/chains/1/signatures", bytes.NewReader(body)) + s.mockSignatureCacher = mock_handlers.NewMockSignatureCacher(ctrl) + s.handler = handlers.NewStatusHandler(s.mockSignatureCacher, chains) +} + +func (s *StatusHandlerTestSuite) Test_HandleRequest_MissingDepositID() { + req := httptest.NewRequest(http.MethodGet, "/v1/chains/1/signatures", nil) + req.Header.Set("Content-Type", "application/json") req = mux.SetURLVars(req, map[string]string{ "chainId": "1", }) - req.Header.Set("Content-Type", "application/json") recorder := httptest.NewRecorder() - go func() { - msg := <-msgChn - ad := msg[0].Data.(*across.LifiEscrowData) - ad.ErrChn <- nil - }() - - handler.HandleSigning(recorder, req) + s.handler.HandleRequest(recorder, req) - s.Equal(http.StatusAccepted, recorder.Code) + s.Equal(http.StatusBadRequest, recorder.Code) } -func (s *SigningHandlerTestSuite) Test_HandleSigning_LighterSuccess() { - msgChn := make(chan []*message.Message) - handler := handlers.NewSigningHandler(msgChn, s.chains) - - input := handlers.SigningBody{ - DepositId: "depositID", - Protocol: "lighter", - LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - Calldata: "0xbe5", - Nonce: &handlers.BigInt{big.NewInt(1001)}, - BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, - //nolint:gosec - Deadline: uint64(time.Now().Unix()), - } - body, _ := json.Marshal(input) - - req := httptest.NewRequest(http.MethodPost, "/v1/chains/1/signatures", bytes.NewReader(body)) +func (s *StatusHandlerTestSuite) Test_HandleRequest_InvalidChainID() { + req := httptest.NewRequest(http.MethodGet, "/v1/chains/invalid/signatures", nil) req = mux.SetURLVars(req, map[string]string{ - "chainId": "1", + "chainId": "invalid", + "depositId": "id", }) - req.Header.Set("Content-Type", "application/json") recorder := httptest.NewRecorder() - go func() { - msg := <-msgChn - ad := msg[0].Data.(*lighter.LighterData) - ad.ErrChn <- nil - }() - - handler.HandleSigning(recorder, req) + s.handler.HandleRequest(recorder, req) - s.Equal(http.StatusAccepted, recorder.Code) + s.Equal(http.StatusBadRequest, recorder.Code) } -func (s *SigningHandlerTestSuite) Test_HandleSigning_SprinterSuccess() { - msgChn := make(chan []*message.Message) - handler := handlers.NewSigningHandler(msgChn, s.chains) - - input := handlers.SigningBody{ - DepositId: "depositID", - Protocol: "sprinter-credit", - LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - Calldata: "0xbe5", - Nonce: &handlers.BigInt{big.NewInt(1001)}, - BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, - //nolint:gosec - Deadline: uint64(time.Now().Unix()), - } - body, _ := json.Marshal(input) - - req := httptest.NewRequest(http.MethodPost, "/v1/chains/1/signatures", bytes.NewReader(body)) +func (s *StatusHandlerTestSuite) Test_HandleRequest_ChainNotSupported() { + req := httptest.NewRequest(http.MethodGet, "/v1/chains/3/signatures", nil) req = mux.SetURLVars(req, map[string]string{ - "chainId": "1", + "chainId": "3", + "depositId": "id", }) - req.Header.Set("Content-Type", "application/json") recorder := httptest.NewRecorder() - go func() { - msg := <-msgChn - ad := msg[0].Data.(*across.SprinterCreditData) - ad.ErrChn <- nil - }() - - handler.HandleSigning(recorder, req) - - s.Equal(http.StatusAccepted, recorder.Code) -} - -type StatusHandlerTestSuite struct { - suite.Suite - - mockSignatureCacher *mock_handlers.MockSignatureCacher - handler *handlers.StatusHandler -} - -func TestRunStatusHandlerTestSuite(t *testing.T) { - suite.Run(t, new(StatusHandlerTestSuite)) -} - -func (s *StatusHandlerTestSuite) SetupTest() { - ctrl := gomock.NewController(s.T()) - defer ctrl.Finish() - - chains := make(map[uint64]struct{}) - chains[1] = struct{}{} + s.handler.HandleRequest(recorder, req) - s.mockSignatureCacher = mock_handlers.NewMockSignatureCacher(ctrl) - s.handler = handlers.NewStatusHandler(s.mockSignatureCacher, chains) + s.Equal(http.StatusNotFound, recorder.Code) } -func (s *StatusHandlerTestSuite) Test_HandleRequest_Errors() { - tests := []struct { - name string - vars map[string]string - query string - wantCode int - }{ - { - name: "missing deposit id", - vars: map[string]string{"chainId": "1"}, - wantCode: http.StatusBadRequest, - }, - { - name: "invalid chain id", - vars: map[string]string{"chainId": "invalid", "depositId": "id"}, - wantCode: http.StatusBadRequest, - }, - { - name: "chain not supported", - vars: map[string]string{"chainId": "3", "depositId": "id"}, - wantCode: http.StatusNotFound, - }, - { - // A partial composite param set must fail fast, not fall back to the legacy key and hang. - name: "partial composite params", - vars: map[string]string{"chainId": "1", "depositId": "id"}, - query: "?deadline=1000&caller=0x1111111111111111111111111111111111111111", - wantCode: http.StatusBadRequest, - }, - } - for _, tt := range tests { - s.Run(tt.name, func() { - req := httptest.NewRequest(http.MethodGet, "/v1/chains/1/signatures/id"+tt.query, nil) - req = mux.SetURLVars(req, tt.vars) - recorder := httptest.NewRecorder() +func (s *StatusHandlerTestSuite) Test_HandleRequest_ValidSignature() { + req := httptest.NewRequest(http.MethodGet, "/v1/chains/1/signatures/id", nil) + req = mux.SetURLVars(req, map[string]string{ + "chainId": "1", + "depositId": "id", + }) - s.handler.HandleRequest(recorder, req) + recorder := httptest.NewRecorder() - s.Equal(tt.wantCode, recorder.Code) + expectedSignature := []byte{0x01, 0x02, 0x03} + s.mockSignatureCacher.EXPECT(). + Subscribe(gomock.Any(), "1-id", gomock.Any()). + Do(func(ctx context.Context, id string, sigChannel chan []byte) { + go func() { + sigChannel <- expectedSignature + }() }) - } -} -func (s *StatusHandlerTestSuite) Test_HandleRequest_SubscribeKey() { - caller := common.HexToAddress("0x1111111111111111111111111111111111111111") - pool := common.HexToAddress("0x3333333333333333333333333333333333333333") - lowerCaller := common.HexToAddress("0xabcdef0123456789abcdef0123456789abcdef01") - lowerPool := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + go s.handler.HandleRequest(recorder, req) - tests := []struct { - name string - depositId string - query string - wantKey string - }{ - { - name: "legacy key without params", - depositId: "id", - wantKey: "1-id", - }, - { - name: "composite key from params", - depositId: "id", - query: "?deadline=1000&caller=" + caller.Hex() + "&borrowAmount=500&liquidityPool=" + pool.Hex() + "&repaymentChainId=10", - wantKey: signature.BorrowSessionID(1, "id", 1000, caller, big.NewInt(500), pool, 10), - }, - { - // Lowercase addresses and a leading-zero deposit id must still map to the canonical publish key. - name: "composite key normalizes address casing and deposit id", - depositId: "0042", - query: "?deadline=1000&caller=0xabcdef0123456789abcdef0123456789abcdef01&borrowAmount=500&liquidityPool=0x1234567890abcdef1234567890abcdef12345678&repaymentChainId=10", - wantKey: signature.BorrowSessionID(1, "42", 1000, lowerCaller, big.NewInt(500), lowerPool, 10), - }, - } - for _, tt := range tests { - s.Run(tt.name, func() { - req := httptest.NewRequest(http.MethodGet, "/v1/chains/1/signatures/"+tt.depositId+tt.query, nil) - req = mux.SetURLVars(req, map[string]string{"chainId": "1", "depositId": tt.depositId}) - recorder := httptest.NewRecorder() + time.Sleep(100 * time.Millisecond) // Give some time for the goroutine to execute - expectedSignature := []byte{0x01, 0x02, 0x03} - s.mockSignatureCacher.EXPECT(). - Subscribe(gomock.Any(), tt.wantKey, gomock.Any()). - Do(func(ctx context.Context, id string, sigChannel chan []byte) { - go func() { - sigChannel <- expectedSignature - }() - }) - - go s.handler.HandleRequest(recorder, req) - - time.Sleep(50 * time.Millisecond) - - s.Equal(http.StatusOK, recorder.Code) - s.Equal("text/event-stream", recorder.Header().Get("Content-Type")) - s.Equal("no-cache", recorder.Header().Get("Cache-Control")) - s.Equal("keep-alive", recorder.Header().Get("Connection")) - s.Equal("*", recorder.Header().Get("Access-Control-Allow-Origin")) - s.Equal("data: "+hex.EncodeToString(expectedSignature)+"\n\n", recorder.Body.String()) - }) - } + s.Equal(http.StatusOK, recorder.Code) + s.Equal("text/event-stream", recorder.Header().Get("Content-Type")) + s.Equal("no-cache", recorder.Header().Get("Cache-Control")) + s.Equal("keep-alive", recorder.Header().Get("Connection")) + s.Equal("*", recorder.Header().Get("Access-Control-Allow-Origin")) + s.Equal("data: "+hex.EncodeToString(expectedSignature)+"\n\n", recorder.Body.String()) } diff --git a/app/app.go b/app/app.go index f4e77dd6..4d6c4c38 100644 --- a/app/app.go +++ b/app/app.go @@ -435,7 +435,7 @@ func Run() error { log.Info().Msg("Relayer not part of MPC. Waiting for refresh event...") } - signingHandler := handlers.NewSigningHandler(msgChan, supportedChains) + signingHandler := handlers.NewSigningHandler(msgChan, supportedChains, signatureCache) statusHandler := handlers.NewStatusHandler(signatureCache, supportedChains) confirmationsHandler := handlers.NewConfirmationsHandler(confirmationsPerChain) unlockHandler := handlers.NewUnlockHandler(msgChan, supportedChains) diff --git a/cache/signature.go b/cache/signature.go index 04b07495..6fa7f12b 100644 --- a/cache/signature.go +++ b/cache/signature.go @@ -68,6 +68,11 @@ func (s *SignatureCache) Subscribe(ctx context.Context, id string, sigChannel ch } } +// Remove drops the cached signature so a new signing round must produce a fresh one. +func (s *SignatureCache) Remove(id string) { + s.sigCache.Delete(id) +} + func (s *SignatureCache) Signature(id string) ([]byte, error) { sig := s.sigCache.Get(id) if sig == nil { diff --git a/cache/signature_test.go b/cache/signature_test.go index 8f43c163..09cd768b 100644 --- a/cache/signature_test.go +++ b/cache/signature_test.go @@ -123,6 +123,24 @@ func (s *SignatureCacheTestSuite) Test_Subscribe_ValidMessage_EarlyExit() { s.Equal(sig, expectedSig.Signature) } +func (s *SignatureCacheTestSuite) Test_Remove_DeletesCachedSignature() { + expectedSig := signing.EcdsaSignature{ + Signature: []byte("signature"), + ID: "signatureID", + } + s.mockMetrics.EXPECT().EndProcess(expectedSig.ID) + s.sigChn <- expectedSig + time.Sleep(time.Millisecond * 100) + + _, err := s.sc.Signature(expectedSig.ID) + s.Nil(err) + + s.sc.Remove(expectedSig.ID) + + _, err = s.sc.Signature(expectedSig.ID) + s.NotNil(err) +} + func (s *SignatureCacheTestSuite) Test_Subscribe_ValidMessage() { expectedSig := signing.EcdsaSignature{ Signature: []byte("signature"), diff --git a/chains/evm/message/across.go b/chains/evm/message/across.go index d98a6f20..806818eb 100644 --- a/chains/evm/message/across.go +++ b/chains/evm/message/across.go @@ -197,9 +197,7 @@ func (h *AcrossMessageHandler) HandleMessage(m *message.Message) (*proposal.Prop return nil, err } - sessionID := signature.BorrowSessionID( - sourceChainID, data.DepositId.String(), data.Deadline, data.Caller, - data.BorrowAmount, data.LiquidityPool, data.RepaymentChainID) + sessionID := fmt.Sprintf("%d-%s", sourceChainID, data.DepositId) signing, err := signing.NewSigning( new(big.Int).SetBytes(unlockHash), sessionID, diff --git a/chains/evm/signature/session.go b/chains/evm/signature/session.go deleted file mode 100644 index 3f9f8268..00000000 --- a/chains/evm/signature/session.go +++ /dev/null @@ -1,18 +0,0 @@ -package signature - -import ( - "fmt" - "math/big" - - "github.com/ethereum/go-ethereum/common" -) - -// BorrowSessionID keys the signing session and cache by the digest fields not already fixed by the deposit. -func BorrowSessionID( - chainID uint64, depositID string, deadline uint64, caller common.Address, - borrowAmount *big.Int, liquidityPool common.Address, repaymentChainID uint64, -) string { - return fmt.Sprintf("%d-%s-%d-%s-%s-%s-%d", - chainID, depositID, deadline, caller.Hex(), - borrowAmount.String(), liquidityPool.Hex(), repaymentChainID) -} diff --git a/chains/evm/signature/session_test.go b/chains/evm/signature/session_test.go deleted file mode 100644 index 4032b88c..00000000 --- a/chains/evm/signature/session_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package signature_test - -import ( - "math/big" - "testing" - - "github.com/ethereum/go-ethereum/common" - "github.com/sprintertech/sprinter-signing/chains/evm/signature" -) - -func TestBorrowSessionID(t *testing.T) { - caller := common.HexToAddress("0x1111111111111111111111111111111111111111") - other := common.HexToAddress("0x2222222222222222222222222222222222222222") - pool := common.HexToAddress("0x3333333333333333333333333333333333333333") - otherPool := common.HexToAddress("0x4444444444444444444444444444444444444444") - id := func(chainID uint64, deposit string, deadline uint64, c common.Address, - amount *big.Int, lp common.Address, repay uint64) string { - return signature.BorrowSessionID(chainID, deposit, deadline, c, amount, lp, repay) - } - base := id(1, "42", 1000, caller, big.NewInt(500), pool, 10) - - tests := []struct { - name string - got string - equal bool - }{ - {"identical inputs match", id(1, "42", 1000, caller, big.NewInt(500), pool, 10), true}, - {"different chain differs", id(2, "42", 1000, caller, big.NewInt(500), pool, 10), false}, - {"different deposit differs", id(1, "43", 1000, caller, big.NewInt(500), pool, 10), false}, - {"different deadline differs", id(1, "42", 1001, caller, big.NewInt(500), pool, 10), false}, - {"different caller differs", id(1, "42", 1000, other, big.NewInt(500), pool, 10), false}, - {"different borrow amount differs", id(1, "42", 1000, caller, big.NewInt(501), pool, 10), false}, - {"different liquidity pool differs", id(1, "42", 1000, caller, big.NewInt(500), otherPool, 10), false}, - {"different repayment chain differs", id(1, "42", 1000, caller, big.NewInt(500), pool, 11), false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if (tt.got == base) != tt.equal { - t.Fatalf("equal=%v, base=%q got=%q", tt.equal, base, tt.got) - } - }) - } - - want := "1-42-1000-" + caller.Hex() + "-500-" + pool.Hex() + "-10" - if base != want { - t.Fatalf("format: want %q got %q", want, base) - } -} From f91c9581aad4c1a07d4b17770cdb4ea2117364db Mon Sep 17 00:00:00 2001 From: Branimir Tomeljak Date: Mon, 20 Jul 2026 15:40:25 +0200 Subject: [PATCH 5/6] comments --- api/handlers/signing.go | 3 --- cache/signature.go | 1 - 2 files changed, 4 deletions(-) diff --git a/api/handlers/signing.go b/api/handlers/signing.go index b45aef2b..7bb06e97 100644 --- a/api/handlers/signing.go +++ b/api/handlers/signing.go @@ -145,7 +145,6 @@ func (h *SigningHandler) HandleSigning(w http.ResponseWriter, r *http.Request) { JSONError(w, fmt.Errorf("invalid protocol %s", b.Protocol), http.StatusBadRequest) return } - // a new signing request supersedes any cached signature for this deposit h.sigCache.Remove(sessionKey(b.Protocol, b.ChainId, b.DepositId)) h.msgChan <- []*message.Message{m} @@ -197,9 +196,7 @@ func (h *SigningHandler) validate(b *SigningBody, vars map[string]string) error return nil } -// sessionKey mirrors the signing session id each protocol handler builds for the cache. func sessionKey(protocol ProtocolType, chainID uint64, depositID string) string { - // lighter signs under its fixed domain id, not the request chain id if protocol == LighterProtocol { return fmt.Sprintf("%d-%s", lighterChain.LIGHTER_DOMAIN_ID, depositID) } diff --git a/cache/signature.go b/cache/signature.go index 6fa7f12b..8f272083 100644 --- a/cache/signature.go +++ b/cache/signature.go @@ -68,7 +68,6 @@ func (s *SignatureCache) Subscribe(ctx context.Context, id string, sigChannel ch } } -// Remove drops the cached signature so a new signing round must produce a fresh one. func (s *SignatureCache) Remove(id string) { s.sigCache.Delete(id) } From 817ca779062023aec4fb84845323c0a565b2560c Mon Sep 17 00:00:00 2001 From: Branimir Tomeljak Date: Mon, 20 Jul 2026 16:21:19 +0200 Subject: [PATCH 6/6] common session id helper --- api/handlers/signing.go | 7 +- api/handlers/signing_test.go | 305 ++++++++++++++++++------------ chains/evm/message/across.go | 2 +- chains/evm/message/lifiEscrow.go | 2 +- chains/evm/message/sprinter.go | 2 +- chains/lighter/message/lighter.go | 2 +- tss/ecdsa/signing/signing.go | 4 + 7 files changed, 191 insertions(+), 133 deletions(-) diff --git a/api/handlers/signing.go b/api/handlers/signing.go index 7bb06e97..a3a89b53 100644 --- a/api/handlers/signing.go +++ b/api/handlers/signing.go @@ -13,6 +13,7 @@ import ( evmMessage "github.com/sprintertech/sprinter-signing/chains/evm/message" lighterChain "github.com/sprintertech/sprinter-signing/chains/lighter" lighterMessage "github.com/sprintertech/sprinter-signing/chains/lighter/message" + "github.com/sprintertech/sprinter-signing/tss/ecdsa/signing" "github.com/sygmaprotocol/sygma-core/relayer/message" ) @@ -198,9 +199,9 @@ func (h *SigningHandler) validate(b *SigningBody, vars map[string]string) error func sessionKey(protocol ProtocolType, chainID uint64, depositID string) string { if protocol == LighterProtocol { - return fmt.Sprintf("%d-%s", lighterChain.LIGHTER_DOMAIN_ID, depositID) + return signing.SessionID(lighterChain.LIGHTER_DOMAIN_ID, depositID) } - return fmt.Sprintf("%d-%s", chainID, depositID) + return signing.SessionID(chainID, depositID) } type SignatureCacher interface { @@ -249,7 +250,7 @@ func (h *StatusHandler) HandleRequest(w http.ResponseWriter, r *http.Request) { ctx := r.Context() sigChn := make(chan []byte, 1) - h.cache.Subscribe(ctx, fmt.Sprintf("%d-%s", chainId, depositId), sigChn) + h.cache.Subscribe(ctx, signing.SessionID(chainId.Uint64(), depositId), sigChn) for { select { case <-r.Context().Done(): diff --git a/api/handlers/signing_test.go b/api/handlers/signing_test.go index fd356c0f..1c1f7be9 100644 --- a/api/handlers/signing_test.go +++ b/api/handlers/signing_test.go @@ -231,141 +231,194 @@ func (s *SigningHandlerTestSuite) Test_HandleSigning_InvalidProtocol() { s.Equal(http.StatusBadRequest, recorder.Code) } -func (s *SigningHandlerTestSuite) Test_HandleSigning_DispatchesAndInvalidates() { - tests := []struct { - name string - body handlers.SigningBody - wantKey string - respond func(msg []*message.Message) - wantCode int - }{ - { - name: "across signing error", - body: handlers.SigningBody{ - DepositId: "1000", - Protocol: "across", - LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - Nonce: &handlers.BigInt{big.NewInt(1001)}, - BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, - //nolint:gosec - Deadline: uint64(time.Now().Unix()), - }, - wantKey: "1-1000", - respond: func(msg []*message.Message) { - ad := msg[0].Data.(*across.AcrossData) - ad.ErrChn <- fmt.Errorf("error handling message") - }, - wantCode: http.StatusInternalServerError, - }, - { - name: "across success", - body: handlers.SigningBody{ - DepositId: "1000", - Protocol: "across", - LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, - Nonce: &handlers.BigInt{big.NewInt(1001)}, - RepaymentChainId: 5, - //nolint:gosec - Deadline: uint64(time.Now().Unix()), - }, - wantKey: "1-1000", - respond: func(msg []*message.Message) { - ad := msg[0].Data.(*across.AcrossData) - s.Equal(ad.RepaymentChainID, uint64(5)) - ad.ErrChn <- nil - }, - wantCode: http.StatusAccepted, - }, - { - name: "lifi-escrow success", - body: handlers.SigningBody{ - DepositId: "depositID", - Protocol: "lifi-escrow", - LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - Calldata: "0xbe5", - Nonce: &handlers.BigInt{big.NewInt(1001)}, - BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, - //nolint:gosec - Deadline: uint64(time.Now().Unix()), - }, - wantKey: "1-depositID", - respond: func(msg []*message.Message) { - ad := msg[0].Data.(*across.LifiEscrowData) - ad.ErrChn <- nil - }, - wantCode: http.StatusAccepted, - }, - { - name: "lighter success", - body: handlers.SigningBody{ - DepositId: "depositID", - Protocol: "lighter", - LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - Calldata: "0xbe5", - Nonce: &handlers.BigInt{big.NewInt(1001)}, - BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, - //nolint:gosec - Deadline: uint64(time.Now().Unix()), - }, - wantKey: fmt.Sprintf("%d-depositID", lighterChain.LIGHTER_DOMAIN_ID), - respond: func(msg []*message.Message) { - ad := msg[0].Data.(*lighter.LighterData) - ad.ErrChn <- nil - }, - wantCode: http.StatusAccepted, - }, - { - name: "sprinter-credit success", - body: handlers.SigningBody{ - DepositId: "depositID", - Protocol: "sprinter-credit", - LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", - Calldata: "0xbe5", - Nonce: &handlers.BigInt{big.NewInt(1001)}, - BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, - //nolint:gosec - Deadline: uint64(time.Now().Unix()), - }, - wantKey: "1-depositID", - respond: func(msg []*message.Message) { - ad := msg[0].Data.(*across.SprinterCreditData) - ad.ErrChn <- nil - }, - wantCode: http.StatusAccepted, - }, +func (s *SigningHandlerTestSuite) Test_HandleSigning_ErrorHandlingMessage() { + msgChn := make(chan []*message.Message) + handler := handlers.NewSigningHandler(msgChn, s.chains, s.mockRemover) + + input := handlers.SigningBody{ + DepositId: "1000", + Protocol: "across", + LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Nonce: &handlers.BigInt{big.NewInt(1001)}, + BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, + //nolint:gosec + Deadline: uint64(time.Now().Unix()), } + body, _ := json.Marshal(input) - for _, tt := range tests { - s.Run(tt.name, func() { - msgChn := make(chan []*message.Message) - handler := handlers.NewSigningHandler(msgChn, s.chains, s.mockRemover) + req := httptest.NewRequest(http.MethodPost, "/v1/chains/1/signatures", bytes.NewReader(body)) + req = mux.SetURLVars(req, map[string]string{ + "chainId": "1", + }) + req.Header.Set("Content-Type", "application/json") - body, _ := json.Marshal(tt.body) - req := httptest.NewRequest(http.MethodPost, "/v1/chains/1/signatures", bytes.NewReader(body)) - req = mux.SetURLVars(req, map[string]string{ - "chainId": "1", - }) - req.Header.Set("Content-Type", "application/json") + recorder := httptest.NewRecorder() - recorder := httptest.NewRecorder() + go func() { + msg := <-msgChn + ad := msg[0].Data.(*across.AcrossData) + ad.ErrChn <- fmt.Errorf("error handling message") + }() - go func() { - tt.respond(<-msgChn) - }() + s.mockRemover.EXPECT().Remove("1-1000") + + handler.HandleSigning(recorder, req) - s.mockRemover.EXPECT().Remove(tt.wantKey) + s.Equal(http.StatusInternalServerError, recorder.Code) +} - handler.HandleSigning(recorder, req) +func (s *SigningHandlerTestSuite) Test_HandleSigning_AcrossSuccess() { + msgChn := make(chan []*message.Message) + handler := handlers.NewSigningHandler(msgChn, s.chains, s.mockRemover) - s.Equal(tt.wantCode, recorder.Code) - }) + input := handlers.SigningBody{ + DepositId: "1000", + Protocol: "across", + LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, + Nonce: &handlers.BigInt{big.NewInt(1001)}, + RepaymentChainId: 5, + //nolint:gosec + Deadline: uint64(time.Now().Unix()), } + body, _ := json.Marshal(input) + + req := httptest.NewRequest(http.MethodPost, "/v1/chains/1/signatures", bytes.NewReader(body)) + req = mux.SetURLVars(req, map[string]string{ + "chainId": "1", + }) + req.Header.Set("Content-Type", "application/json") + + recorder := httptest.NewRecorder() + + go func() { + msg := <-msgChn + ad := msg[0].Data.(*across.AcrossData) + s.Equal(ad.RepaymentChainID, uint64(5)) + ad.ErrChn <- nil + }() + + s.mockRemover.EXPECT().Remove("1-1000") + + handler.HandleSigning(recorder, req) + + s.Equal(http.StatusAccepted, recorder.Code) +} + +func (s *SigningHandlerTestSuite) Test_HandleSigning_LifiSuccess() { + msgChn := make(chan []*message.Message) + handler := handlers.NewSigningHandler(msgChn, s.chains, s.mockRemover) + + input := handlers.SigningBody{ + DepositId: "depositID", + Protocol: "lifi-escrow", + LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Calldata: "0xbe5", + Nonce: &handlers.BigInt{big.NewInt(1001)}, + BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, + //nolint:gosec + Deadline: uint64(time.Now().Unix()), + } + body, _ := json.Marshal(input) + + req := httptest.NewRequest(http.MethodPost, "/v1/chains/1/signatures", bytes.NewReader(body)) + req = mux.SetURLVars(req, map[string]string{ + "chainId": "1", + }) + req.Header.Set("Content-Type", "application/json") + + recorder := httptest.NewRecorder() + + go func() { + msg := <-msgChn + ad := msg[0].Data.(*across.LifiEscrowData) + ad.ErrChn <- nil + }() + + s.mockRemover.EXPECT().Remove("1-depositID") + + handler.HandleSigning(recorder, req) + + s.Equal(http.StatusAccepted, recorder.Code) +} + +func (s *SigningHandlerTestSuite) Test_HandleSigning_LighterSuccess() { + msgChn := make(chan []*message.Message) + handler := handlers.NewSigningHandler(msgChn, s.chains, s.mockRemover) + + input := handlers.SigningBody{ + DepositId: "depositID", + Protocol: "lighter", + LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Calldata: "0xbe5", + Nonce: &handlers.BigInt{big.NewInt(1001)}, + BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, + //nolint:gosec + Deadline: uint64(time.Now().Unix()), + } + body, _ := json.Marshal(input) + + req := httptest.NewRequest(http.MethodPost, "/v1/chains/1/signatures", bytes.NewReader(body)) + req = mux.SetURLVars(req, map[string]string{ + "chainId": "1", + }) + req.Header.Set("Content-Type", "application/json") + + recorder := httptest.NewRecorder() + + go func() { + msg := <-msgChn + ad := msg[0].Data.(*lighter.LighterData) + ad.ErrChn <- nil + }() + + s.mockRemover.EXPECT().Remove(fmt.Sprintf("%d-depositID", lighterChain.LIGHTER_DOMAIN_ID)) + + handler.HandleSigning(recorder, req) + + s.Equal(http.StatusAccepted, recorder.Code) +} + +func (s *SigningHandlerTestSuite) Test_HandleSigning_SprinterSuccess() { + msgChn := make(chan []*message.Message) + handler := handlers.NewSigningHandler(msgChn, s.chains, s.mockRemover) + + input := handlers.SigningBody{ + DepositId: "depositID", + Protocol: "sprinter-credit", + LiquidityPool: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Caller: "0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657", + Calldata: "0xbe5", + Nonce: &handlers.BigInt{big.NewInt(1001)}, + BorrowAmount: &handlers.BigInt{big.NewInt(1000)}, + //nolint:gosec + Deadline: uint64(time.Now().Unix()), + } + body, _ := json.Marshal(input) + + req := httptest.NewRequest(http.MethodPost, "/v1/chains/1/signatures", bytes.NewReader(body)) + req = mux.SetURLVars(req, map[string]string{ + "chainId": "1", + }) + req.Header.Set("Content-Type", "application/json") + + recorder := httptest.NewRecorder() + + go func() { + msg := <-msgChn + ad := msg[0].Data.(*across.SprinterCreditData) + ad.ErrChn <- nil + }() + + s.mockRemover.EXPECT().Remove("1-depositID") + + handler.HandleSigning(recorder, req) + + s.Equal(http.StatusAccepted, recorder.Code) } type StatusHandlerTestSuite struct { diff --git a/chains/evm/message/across.go b/chains/evm/message/across.go index 806818eb..3632b364 100644 --- a/chains/evm/message/across.go +++ b/chains/evm/message/across.go @@ -197,7 +197,7 @@ func (h *AcrossMessageHandler) HandleMessage(m *message.Message) (*proposal.Prop return nil, err } - sessionID := fmt.Sprintf("%d-%s", sourceChainID, data.DepositId) + sessionID := signing.SessionID(sourceChainID, data.DepositId.String()) signing, err := signing.NewSigning( new(big.Int).SetBytes(unlockHash), sessionID, diff --git a/chains/evm/message/lifiEscrow.go b/chains/evm/message/lifiEscrow.go index 3a6e6956..2100429f 100644 --- a/chains/evm/message/lifiEscrow.go +++ b/chains/evm/message/lifiEscrow.go @@ -180,7 +180,7 @@ func (h *LifiEscrowMessageHandler) HandleMessage(m *message.Message) (*proposal. return nil, err } - sessionID := fmt.Sprintf("%d-%s", h.chainID, data.OrderID) + sessionID := signing.SessionID(h.chainID, data.OrderID) signing, err := signing.NewSigning( new(big.Int).SetBytes(unlockHash), sessionID, diff --git a/chains/evm/message/sprinter.go b/chains/evm/message/sprinter.go index d71bdc76..1bae1b7f 100644 --- a/chains/evm/message/sprinter.go +++ b/chains/evm/message/sprinter.go @@ -94,7 +94,7 @@ func (h *SprinterCreditMessageHandler) HandleMessage(m *message.Message) (*propo } data.ErrChn <- nil - sessionID := fmt.Sprintf("%d-%s", h.chainID, data.DepositID) + sessionID := signing.SessionID(h.chainID, data.DepositID) signing, err := signing.NewSigning( new(big.Int).SetBytes(unlockHash), sessionID, diff --git a/chains/lighter/message/lighter.go b/chains/lighter/message/lighter.go index 12db97a2..c75d7d8f 100644 --- a/chains/lighter/message/lighter.go +++ b/chains/lighter/message/lighter.go @@ -124,7 +124,7 @@ func (h *LighterMessageHandler) HandleMessage(m *message.Message) (*proposal.Pro return nil, err } - sessionID := fmt.Sprintf("%d-%s", lighterChain.LIGHTER_DOMAIN_ID, data.OrderHash) + sessionID := signing.SessionID(lighterChain.LIGHTER_DOMAIN_ID, data.OrderHash) signing, err := signing.NewSigning( new(big.Int).SetBytes(unlockHash), sessionID, diff --git a/tss/ecdsa/signing/signing.go b/tss/ecdsa/signing/signing.go index ea7fe6e3..b9c86739 100644 --- a/tss/ecdsa/signing/signing.go +++ b/tss/ecdsa/signing/signing.go @@ -50,6 +50,10 @@ type Signing struct { subscriptionID comm.SubscriptionID } +func SessionID(chainID uint64, depositID string) string { + return fmt.Sprintf("%d-%s", chainID, depositID) +} + func NewSigning( msg *big.Int, messageID string,