From 96617d3c1b5bf51f82e2a7aeaca3198147f35b44 Mon Sep 17 00:00:00 2001 From: weiihann Date: Thu, 10 Oct 2024 15:46:07 +0800 Subject: [PATCH 01/26] Complete starknet_subscribeNewHeads --- docs/docs/faq.md | 2 +- docs/docs/websocket.md | 9 +- docs/versioned_docs/version-0.11.8/faq.md | 2 +- .../version-0.11.8/websocket.md | 6 +- rpc/events.go | 152 +++++++++++++++--- rpc/events_test.go | 25 +-- rpc/handlers.go | 13 +- 7 files changed, 156 insertions(+), 53 deletions(-) diff --git a/docs/docs/faq.md b/docs/docs/faq.md index 78828677ce..1ae6c6a56f 100644 --- a/docs/docs/faq.md +++ b/docs/docs/faq.md @@ -74,7 +74,7 @@ docker logs -f juno
How can I get real-time updates of new blocks? -The [WebSocket](websocket#subscribe-to-newly-created-blocks) interface provides a `juno_subscribeNewHeads` method that emits an event when new blocks are added to the blockchain. +The [WebSocket](websocket#subscribe-to-newly-created-blocks) interface provides a `starknet_subscribeNewHeads` method that emits an event when new blocks are added to the blockchain.
diff --git a/docs/docs/websocket.md b/docs/docs/websocket.md index ba55e24db8..4614caedc0 100644 --- a/docs/docs/websocket.md +++ b/docs/docs/websocket.md @@ -96,7 +96,7 @@ Get the most recent accepted block hash and number with the `starknet_blockHashA ## Subscribe to newly created blocks -The WebSocket server provides a `juno_subscribeNewHeads` method that emits an event when new blocks are added to the blockchain: +The WebSocket server provides a `starknet_subscribeNewHeads` method that emits an event when new blocks are added to the blockchain: @@ -104,8 +104,7 @@ The WebSocket server provides a `juno_subscribeNewHeads` method that emits an ev ```json { "jsonrpc": "2.0", - "method": "juno_subscribeNewHeads", - "params": [], + "method": "starknet_subscribeNewHeads", "id": 1 } ``` @@ -129,7 +128,7 @@ When a new block is added, you will receive a message like this: ```json { "jsonrpc": "2.0", - "method": "juno_subscribeNewHeads", + "method": "starknet_subscriptionNewHeads", "params": { "result": { "block_hash": "0x840660a07a17ae6a55d39fb6d366698ecda11e02280ca3e9ca4b4f1bad741c", @@ -149,7 +148,7 @@ When a new block is added, you will receive a message like this: "l1_da_mode": "BLOB", "starknet_version": "0.13.1.1" }, - "subscription": 16570962336122680234 + "subscription_id": 16570962336122680234 } } ``` diff --git a/docs/versioned_docs/version-0.11.8/faq.md b/docs/versioned_docs/version-0.11.8/faq.md index 78828677ce..1ae6c6a56f 100644 --- a/docs/versioned_docs/version-0.11.8/faq.md +++ b/docs/versioned_docs/version-0.11.8/faq.md @@ -74,7 +74,7 @@ docker logs -f juno
How can I get real-time updates of new blocks? -The [WebSocket](websocket#subscribe-to-newly-created-blocks) interface provides a `juno_subscribeNewHeads` method that emits an event when new blocks are added to the blockchain. +The [WebSocket](websocket#subscribe-to-newly-created-blocks) interface provides a `starknet_subscribeNewHeads` method that emits an event when new blocks are added to the blockchain.
diff --git a/docs/versioned_docs/version-0.11.8/websocket.md b/docs/versioned_docs/version-0.11.8/websocket.md index ba55e24db8..6a0ea3de4f 100644 --- a/docs/versioned_docs/version-0.11.8/websocket.md +++ b/docs/versioned_docs/version-0.11.8/websocket.md @@ -96,7 +96,7 @@ Get the most recent accepted block hash and number with the `starknet_blockHashA ## Subscribe to newly created blocks -The WebSocket server provides a `juno_subscribeNewHeads` method that emits an event when new blocks are added to the blockchain: +The WebSocket server provides a `starknet_subscribeNewHeads` method that emits an event when new blocks are added to the blockchain: @@ -104,7 +104,7 @@ The WebSocket server provides a `juno_subscribeNewHeads` method that emits an ev ```json { "jsonrpc": "2.0", - "method": "juno_subscribeNewHeads", + "method": "starknet_subscribeNewHeads", "params": [], "id": 1 } @@ -129,7 +129,7 @@ When a new block is added, you will receive a message like this: ```json { "jsonrpc": "2.0", - "method": "juno_subscribeNewHeads", + "method": "starknet_subscribeNewHeads", "params": { "result": { "block_hash": "0x840660a07a17ae6a55d39fb6d366698ecda11e02280ca3e9ca4b4f1bad741c", diff --git a/rpc/events.go b/rpc/events.go index a7298486f8..ec053b546a 100644 --- a/rpc/events.go +++ b/rpc/events.go @@ -5,10 +5,16 @@ import ( "encoding/json" "github.com/NethermindEth/juno/blockchain" + "github.com/NethermindEth/juno/core" "github.com/NethermindEth/juno/core/felt" + "github.com/NethermindEth/juno/feed" "github.com/NethermindEth/juno/jsonrpc" ) +const ( + MaxBlocksBack = 1024 +) + type EventsArg struct { EventFilter ResultPageRequest @@ -52,10 +58,15 @@ type SubscriptionID struct { Events Handlers *****************************************************/ -func (h *Handler) SubscribeNewHeads(ctx context.Context) (uint64, *jsonrpc.Error) { +func (h *Handler) SubscribeNewHeads(ctx context.Context, blockID *BlockID) (*SubscriptionID, *jsonrpc.Error) { w, ok := jsonrpc.ConnFromContext(ctx) if !ok { - return 0, jsonrpc.Err(jsonrpc.MethodNotFound, nil) + return nil, jsonrpc.Err(jsonrpc.MethodNotFound, nil) + } + + startHeader, latestHeader, rpcErr := h.getStartAndLatestHeaders(blockID) + if rpcErr != nil { + return nil, rpcErr } id := h.idgen() @@ -67,37 +78,130 @@ func (h *Handler) SubscribeNewHeads(ctx context.Context) (uint64, *jsonrpc.Error h.mu.Lock() h.subscriptions[id] = sub h.mu.Unlock() + headerSub := h.newHeads.Subscribe() sub.wg.Go(func() { defer func() { - headerSub.Unsubscribe() h.unsubscribe(sub, id) + headerSub.Unsubscribe() }() - for { - select { - case <-subscriptionCtx.Done(): + + newHeadersChan := make(chan *core.Header, MaxBlocksBack) + + sub.wg.Go(func() { + h.bufferNewHeaders(subscriptionCtx, headerSub, newHeadersChan) + }) + + if err := h.sendHistoricalHeaders(subscriptionCtx, startHeader, latestHeader, w, id); err != nil { + h.log.Errorw("Error sending old headers", "err", err) + return + } + + h.processNewHeaders(subscriptionCtx, newHeadersChan, w, id) + }) + + return &SubscriptionID{ID: id}, nil +} + +// getStartAndLatestHeaders gets the start and latest header for the subscription +func (h *Handler) getStartAndLatestHeaders(blockID *BlockID) (*core.Header, *core.Header, *jsonrpc.Error) { + if blockID == nil || blockID.Latest { + return nil, nil, nil + } + + latestHeader, err := h.bcReader.HeadsHeader() + if err != nil { + return nil, nil, ErrInternal + } + + startHeader, rpcErr := h.blockHeaderByID(blockID) + if rpcErr != nil { + return nil, nil, rpcErr + } + + if latestHeader.Number > MaxBlocksBack && startHeader.Number < latestHeader.Number-MaxBlocksBack { + return nil, nil, ErrTooManyBlocksBack + } + + return startHeader, latestHeader, nil +} + +// sendHistoricalHeaders sends a range of headers from the start header until the latest header +func (h *Handler) sendHistoricalHeaders( + ctx context.Context, + startHeader *core.Header, + latestHeader *core.Header, + w jsonrpc.Conn, + id uint64, +) error { + if startHeader == nil { + return nil + } + + var err error + + lastHeader := startHeader + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + if err := h.sendHeader(w, lastHeader, id); err != nil { + return err + } + + if lastHeader.Number == latestHeader.Number { + return nil + } + + lastHeader, err = h.bcReader.BlockHeaderByNumber(lastHeader.Number + 1) + if err != nil { + return err + } + } + } +} + +func (h *Handler) bufferNewHeaders(ctx context.Context, headerSub *feed.Subscription[*core.Header], newHeadersChan chan<- *core.Header) { + for { + select { + case <-ctx.Done(): + return + case header := <-headerSub.Recv(): + newHeadersChan <- header + } + } +} + +func (h *Handler) processNewHeaders(ctx context.Context, newHeadersChan <-chan *core.Header, w jsonrpc.Conn, id uint64) { + for { + select { + case <-ctx.Done(): + return + case header := <-newHeadersChan: + if err := h.sendHeader(w, header, id); err != nil { + h.log.Warnw("Error sending header", "err", err) return - case header := <-headerSub.Recv(): - resp, err := json.Marshal(SubscriptionResponse{ - Version: "2.0", - Method: "juno_subscribeNewHeads", - Params: map[string]any{ - "result": adaptBlockHeader(header), - "subscription": id, - }, - }) - if err != nil { - h.log.Warnw("Error marshalling a subscription reply", "err", err) - return - } - if _, err = w.Write(resp); err != nil { - h.log.Warnw("Error writing a subscription reply", "err", err) - return - } } } + } +} + +// sendHeader creates a request and sends it to the client +func (h *Handler) sendHeader(w jsonrpc.Conn, header *core.Header, id uint64) error { + resp, err := json.Marshal(jsonrpc.Request{ + Version: "2.0", + Method: "starknet_subscriptionNewHeads", + Params: map[string]any{ + "subscription_id": id, + "result": adaptBlockHeader(header), + }, }) - return id, nil + if err != nil { + return err + } + _, err = w.Write(resp) + return err } func (h *Handler) Unsubscribe(ctx context.Context, id uint64) (bool, *jsonrpc.Error) { diff --git a/rpc/events_test.go b/rpc/events_test.go index c2f1417791..8cee84d73d 100644 --- a/rpc/events_test.go +++ b/rpc/events_test.go @@ -258,7 +258,7 @@ func TestSubscribeNewHeadsAndUnsubscribe(t *testing.T) { }) // Subscribe without setting the connection on the context. - id, rpcErr := handler.SubscribeNewHeads(ctx) + id, rpcErr := handler.SubscribeNewHeads(ctx, nil) require.Zero(t, id) require.Equal(t, jsonrpc.MethodNotFound, rpcErr.Code) @@ -274,7 +274,7 @@ func TestSubscribeNewHeadsAndUnsubscribe(t *testing.T) { // Subscribe. subCtx := context.WithValue(ctx, jsonrpc.ConnKey{}, &fakeConn{w: serverConn}) - id, rpcErr = handler.SubscribeNewHeads(subCtx) + id, rpcErr = handler.SubscribeNewHeads(subCtx, nil) require.Nil(t, rpcErr) // Sync the block we reverted above. @@ -283,32 +283,32 @@ func TestSubscribeNewHeadsAndUnsubscribe(t *testing.T) { syncCancel() // Receive a block header. - want := `{"jsonrpc":"2.0","method":"juno_subscribeNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription":%d}}` - want = fmt.Sprintf(want, id) + want := `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` + want = fmt.Sprintf(want, id.ID) got := make([]byte, len(want)) _, err := clientConn.Read(got) require.NoError(t, err) require.Equal(t, want, string(got)) // Unsubscribe without setting the connection on the context. - ok, rpcErr := handler.Unsubscribe(ctx, id) + ok, rpcErr := handler.Unsubscribe(ctx, id.ID) require.Equal(t, jsonrpc.MethodNotFound, rpcErr.Code) require.False(t, ok) // Unsubscribe on correct connection with the incorrect id. - ok, rpcErr = handler.Unsubscribe(subCtx, id+1) + ok, rpcErr = handler.Unsubscribe(subCtx, id.ID+1) require.Equal(t, rpc.ErrSubscriptionNotFound, rpcErr) require.False(t, ok) // Unsubscribe on incorrect connection with the correct id. subCtx = context.WithValue(context.Background(), jsonrpc.ConnKey{}, &fakeConn{}) - ok, rpcErr = handler.Unsubscribe(subCtx, id) + ok, rpcErr = handler.Unsubscribe(subCtx, id.ID) require.Equal(t, rpc.ErrSubscriptionNotFound, rpcErr) require.False(t, ok) // Unsubscribe on correct connection with the correct id. subCtx = context.WithValue(context.Background(), jsonrpc.ConnKey{}, &fakeConn{w: serverConn}) - ok, rpcErr = handler.Unsubscribe(subCtx, id) + ok, rpcErr = handler.Unsubscribe(subCtx, id.ID) require.Nil(t, rpcErr) require.True(t, ok) } @@ -344,7 +344,8 @@ func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { server := jsonrpc.NewServer(1, log) require.NoError(t, server.RegisterMethods(jsonrpc.Method{ - Name: "juno_subscribeNewHeads", + Name: "starknet_subscribeNewHeads", + Params: []jsonrpc.Parameter{{Name: "block", Optional: true}}, Handler: handler.SubscribeNewHeads, }, jsonrpc.Method{ Name: "juno_unsubscribe", @@ -358,14 +359,14 @@ func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { conn2, _, err := websocket.Dial(ctx, httpSrv.URL, nil) require.NoError(t, err) - subscribeMsg := []byte(`{"jsonrpc":"2.0","id":1,"method":"juno_subscribeNewHeads"}`) + subscribeMsg := []byte(`{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads"}`) firstID := uint64(1) secondID := uint64(2) handler.WithIDGen(func() uint64 { return firstID }) require.NoError(t, conn1.Write(ctx, websocket.MessageText, subscribeMsg)) - want := `{"jsonrpc":"2.0","result":%d,"id":1}` + want := `{"jsonrpc":"2.0","result":{"subscription_id":%d},"id":1}` firstWant := fmt.Sprintf(want, firstID) _, firstGot, err := conn1.Read(ctx) require.NoError(t, err) @@ -384,7 +385,7 @@ func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { syncCancel() // Receive a block header. - want = `{"jsonrpc":"2.0","method":"juno_subscribeNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription":%d}}` + want = `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` firstWant = fmt.Sprintf(want, firstID) _, firstGot, err = conn1.Read(ctx) require.NoError(t, err) diff --git a/rpc/handlers.go b/rpc/handlers.go index 1cf96b0c21..79e4d86686 100644 --- a/rpc/handlers.go +++ b/rpc/handlers.go @@ -69,6 +69,8 @@ var ( ErrTooManyBlocksBack = &jsonrpc.Error{Code: 68, Message: fmt.Sprintf("Cannot go back more than %v blocks", maxBlocksBack)} ErrCallOnPending = &jsonrpc.Error{Code: 69, Message: "This method does not support being called on the pending block"} + ErrTooManyBlocksBack = &jsonrpc.Error{Code: 68, Message: "Cannot go back more than 1024 blocks"} + // These errors can be only be returned by Juno-specific methods. ErrSubscriptionNotFound = &jsonrpc.Error{Code: 100, Message: "Subscription not found"} ) @@ -339,12 +341,8 @@ func (h *Handler) Methods() ([]jsonrpc.Method, string) { //nolint: funlen Handler: h.SpecVersion, }, { - Name: "starknet_subscribeEvents", - Params: []jsonrpc.Parameter{{Name: "from_address", Optional: true}, {Name: "keys", Optional: true}, {Name: "block", Optional: true}}, - Handler: h.SubscribeEvents, - }, - { - Name: "juno_subscribeNewHeads", + Name: "starknet_subscribeNewHeads", + Params: []jsonrpc.Parameter{{Name: "block", Optional: true}}, Handler: h.SubscribeNewHeads, }, { @@ -512,7 +510,8 @@ func (h *Handler) MethodsV0_7() ([]jsonrpc.Method, string) { //nolint: funlen Handler: h.SpecVersionV0_7, }, { - Name: "juno_subscribeNewHeads", + Name: "starknet_subscribeNewHeads", + Params: []jsonrpc.Parameter{{Name: "block", Optional: true}}, Handler: h.SubscribeNewHeads, }, { From 1435033e4c08bb4e18daae11bb5aaff330ad189f Mon Sep 17 00:00:00 2001 From: weiihann Date: Mon, 14 Oct 2024 14:42:11 +0800 Subject: [PATCH 02/26] revert versioned docs changes --- docs/versioned_docs/version-0.11.8/faq.md | 2 +- docs/versioned_docs/version-0.11.8/websocket.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/versioned_docs/version-0.11.8/faq.md b/docs/versioned_docs/version-0.11.8/faq.md index 1ae6c6a56f..78828677ce 100644 --- a/docs/versioned_docs/version-0.11.8/faq.md +++ b/docs/versioned_docs/version-0.11.8/faq.md @@ -74,7 +74,7 @@ docker logs -f juno
How can I get real-time updates of new blocks? -The [WebSocket](websocket#subscribe-to-newly-created-blocks) interface provides a `starknet_subscribeNewHeads` method that emits an event when new blocks are added to the blockchain. +The [WebSocket](websocket#subscribe-to-newly-created-blocks) interface provides a `juno_subscribeNewHeads` method that emits an event when new blocks are added to the blockchain.
diff --git a/docs/versioned_docs/version-0.11.8/websocket.md b/docs/versioned_docs/version-0.11.8/websocket.md index 6a0ea3de4f..ba55e24db8 100644 --- a/docs/versioned_docs/version-0.11.8/websocket.md +++ b/docs/versioned_docs/version-0.11.8/websocket.md @@ -96,7 +96,7 @@ Get the most recent accepted block hash and number with the `starknet_blockHashA ## Subscribe to newly created blocks -The WebSocket server provides a `starknet_subscribeNewHeads` method that emits an event when new blocks are added to the blockchain: +The WebSocket server provides a `juno_subscribeNewHeads` method that emits an event when new blocks are added to the blockchain: @@ -104,7 +104,7 @@ The WebSocket server provides a `starknet_subscribeNewHeads` method that emits a ```json { "jsonrpc": "2.0", - "method": "starknet_subscribeNewHeads", + "method": "juno_subscribeNewHeads", "params": [], "id": 1 } @@ -129,7 +129,7 @@ When a new block is added, you will receive a message like this: ```json { "jsonrpc": "2.0", - "method": "starknet_subscribeNewHeads", + "method": "juno_subscribeNewHeads", "params": { "result": { "block_hash": "0x840660a07a17ae6a55d39fb6d366698ecda11e02280ca3e9ca4b4f1bad741c", From 0886cd4768935c2de8b510771a6af94cf869db18 Mon Sep 17 00:00:00 2001 From: weiihann Date: Mon, 14 Oct 2024 17:25:49 +0800 Subject: [PATCH 03/26] Fix empty params false error --- jsonrpc/server.go | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/jsonrpc/server.go b/jsonrpc/server.go index c63f15c849..888c0d687a 100644 --- a/jsonrpc/server.go +++ b/jsonrpc/server.go @@ -426,6 +426,19 @@ func isNil(i any) bool { return i == nil || reflect.ValueOf(i).IsNil() } +func isNilOrEmpty(i any) (bool, error) { + if isNil(i) { + return true, nil + } + + switch reflect.TypeOf(i).Kind() { + case reflect.Slice, reflect.Array, reflect.Map: + return reflect.ValueOf(i).Len() == 0, nil + default: + return false, fmt.Errorf("impossible param type: check request.isSane") + } +} + func (s *Server) handleRequest(ctx context.Context, req *Request) (*response, http.Header, error) { s.log.Tracew("Received request", "req", req) @@ -486,6 +499,7 @@ func (s *Server) handleRequest(ctx context.Context, req *Request) (*response, ht return res, header, nil } +//nolint:gocyclo func (s *Server) buildArguments(ctx context.Context, params any, method Method) ([]reflect.Value, error) { handlerType := reflect.TypeOf(method.Handler) @@ -498,7 +512,12 @@ func (s *Server) buildArguments(ctx context.Context, params any, method Method) addContext = 1 } - if isNil(params) { + isNilOrEmpty, err := isNilOrEmpty(params) + if err != nil { + return nil, err + } + + if isNilOrEmpty { allParamsAreOptional := utils.All(method.Params, func(p Parameter) bool { return p.Optional }) From a77fe7584ab65b5f7c29e46a335b9cbe99a16c99 Mon Sep 17 00:00:00 2001 From: weiihann Date: Mon, 14 Oct 2024 18:36:52 +0800 Subject: [PATCH 04/26] add 1 more test case --- rpc/events_test.go | 190 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 146 insertions(+), 44 deletions(-) diff --git a/rpc/events_test.go b/rpc/events_test.go index 8cee84d73d..d859216275 100644 --- a/rpc/events_test.go +++ b/rpc/events_test.go @@ -14,6 +14,7 @@ import ( "github.com/NethermindEth/juno/core" "github.com/NethermindEth/juno/core/felt" "github.com/NethermindEth/juno/db/pebble" + "github.com/NethermindEth/juno/feed" "github.com/NethermindEth/juno/jsonrpc" "github.com/NethermindEth/juno/rpc" adaptfeeder "github.com/NethermindEth/juno/starknetdata/feeder" @@ -24,6 +25,8 @@ import ( "github.com/stretchr/testify/require" ) +var emptyCommitments = core.BlockCommitments{} + func TestEvents(t *testing.T) { var pendingB *core.Block pendingBlockFn := func() *core.Block { @@ -231,18 +234,31 @@ func (fc *fakeConn) Equal(other jsonrpc.Conn) bool { return fc.w == fc2.w } +type fakeSyncer struct { + newHeads *feed.Feed[*core.Header] +} + +func (fs *fakeSyncer) SubscribeNewHeads() sync.HeaderSubscription { + return sync.HeaderSubscription{Subscription: fs.newHeads.Subscribe()} +} + +func (fs *fakeSyncer) StartingBlockNumber() (uint64, error) { + return 0, nil +} + +func (fs *fakeSyncer) HighestBlockHeader() *core.Header { + return nil +} + func TestSubscribeNewHeadsAndUnsubscribe(t *testing.T) { t.Parallel() - log := utils.NewNopZapLogger() - n := utils.Ptr(utils.Mainnet) - client := feeder.NewTestClient(t, n) - gw := adaptfeeder.New(client) + + chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) + syncer := &fakeSyncer{newHeads: feed.New[*core.Header]()} + handler := rpc.New(chain, syncer, nil, "", utils.NewNopZapLogger()) + ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - testDB := pebble.NewMemTest(t) - chain := blockchain.New(pebble.NewMemTest(t), n, nil) - syncer := sync.New(chain, gw, log, 0, false, testDB) - handler := rpc.New(chain, syncer, nil, "", log) go func() { require.NoError(t, handler.Run(ctx)) @@ -262,25 +278,28 @@ func TestSubscribeNewHeadsAndUnsubscribe(t *testing.T) { require.Zero(t, id) require.Equal(t, jsonrpc.MethodNotFound, rpcErr.Code) - // Sync blocks and then revert head. - // This is a super hacky way to deterministically receive a single block on the subscription. - // It would be nicer if we could tell the synchronizer to exit after a certain block height, but, alas, we can't do that. - syncCtx, syncCancel := context.WithTimeout(context.Background(), time.Second) - require.NoError(t, syncer.Run(syncCtx)) - syncCancel() - // This is technically an unsafe thing to do. We're modifying the synchronizer's blockchain while it is owned by the synchronizer. - // But it works. - require.NoError(t, chain.RevertHead()) - - // Subscribe. + // Subscribe correctly. subCtx := context.WithValue(ctx, jsonrpc.ConnKey{}, &fakeConn{w: serverConn}) id, rpcErr = handler.SubscribeNewHeads(subCtx, nil) require.Nil(t, rpcErr) - // Sync the block we reverted above. - syncCtx, syncCancel = context.WithTimeout(context.Background(), 250*time.Millisecond) - require.NoError(t, syncer.Run(syncCtx)) - syncCancel() + // Simulate a new block + syncer.newHeads.Send(&core.Header{ + Hash: utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), + ParentHash: utils.HexToFelt(t, "0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb"), + Number: 2, + GlobalStateRoot: utils.HexToFelt(t, "0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9"), + Timestamp: 1637084470, + SequencerAddress: utils.HexToFelt(t, "0x0"), + L1DataGasPrice: &core.GasPrice{ + PriceInFri: utils.HexToFelt(t, "0x0"), + PriceInWei: utils.HexToFelt(t, "0x0"), + }, + GasPrice: utils.HexToFelt(t, "0x0"), + GasPriceSTRK: utils.HexToFelt(t, "0x0"), + L1DAMode: core.Calldata, + ProtocolVersion: "", + }) // Receive a block header. want := `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` @@ -315,16 +334,15 @@ func TestSubscribeNewHeadsAndUnsubscribe(t *testing.T) { func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { t.Parallel() + log := utils.NewNopZapLogger() - n := utils.Ptr(utils.Mainnet) - feederClient := feeder.NewTestClient(t, n) - gw := adaptfeeder.New(feederClient) + chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) + syncer := &fakeSyncer{newHeads: feed.New[*core.Header]()} + handler := rpc.New(chain, syncer, nil, "", log) + ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - testDB := pebble.NewMemTest(t) - chain := blockchain.New(testDB, n, nil) - syncer := sync.New(chain, gw, log, 0, false, testDB) - handler := rpc.New(chain, syncer, nil, "", log) + go func() { require.NoError(t, handler.Run(ctx)) }() @@ -332,16 +350,6 @@ func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { // Sleep for a moment just in case. time.Sleep(50 * time.Millisecond) - // Sync blocks and then revert head. - // This is a super hacky way to deterministically receive a single block on the subscription. - // It would be nicer if we could tell the synchronizer to exit after a certain block height, but, alas, we can't do that. - syncCtx, syncCancel := context.WithTimeout(context.Background(), time.Second) - require.NoError(t, syncer.Run(syncCtx)) - syncCancel() - // This is technically an unsafe thing to do. We're modifying the synchronizer's blockchain while it is owned by the synchronizer. - // But it works. - require.NoError(t, chain.RevertHead()) - server := jsonrpc.NewServer(1, log) require.NoError(t, server.RegisterMethods(jsonrpc.Method{ Name: "starknet_subscribeNewHeads", @@ -379,10 +387,23 @@ func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { require.NoError(t, err) require.Equal(t, secondWant, string(secondGot)) - // Now we're subscribed. Sync the block we reverted above. - syncCtx, syncCancel = context.WithTimeout(context.Background(), 250*time.Millisecond) - require.NoError(t, syncer.Run(syncCtx)) - syncCancel() + // Simulate a new block + syncer.newHeads.Send(&core.Header{ + Hash: utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), + ParentHash: utils.HexToFelt(t, "0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb"), + Number: 2, + GlobalStateRoot: utils.HexToFelt(t, "0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9"), + Timestamp: 1637084470, + SequencerAddress: utils.HexToFelt(t, "0x0"), + L1DataGasPrice: &core.GasPrice{ + PriceInFri: utils.HexToFelt(t, "0x0"), + PriceInWei: utils.HexToFelt(t, "0x0"), + }, + GasPrice: utils.HexToFelt(t, "0x0"), + GasPriceSTRK: utils.HexToFelt(t, "0x0"), + L1DAMode: core.Calldata, + ProtocolVersion: "", + }) // Receive a block header. want = `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` @@ -400,3 +421,84 @@ func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { require.NoError(t, conn1.Write(ctx, websocket.MessageBinary, []byte(fmt.Sprintf(unsubMsg, firstID)))) require.NoError(t, conn2.Write(ctx, websocket.MessageBinary, []byte(fmt.Sprintf(unsubMsg, secondID)))) } + +func TestSubscribeNewHeadsHistorical(t *testing.T) { + client := feeder.NewTestClient(t, &utils.Mainnet) + gw := adaptfeeder.New(client) + + block0, err := gw.BlockByNumber(context.Background(), 0) + require.NoError(t, err) + + stateUpdate0, err := gw.StateUpdate(context.Background(), 0) + require.NoError(t, err) + + testDB := pebble.NewMemTest(t) + chain := blockchain.New(testDB, &utils.Mainnet) + assert.NoError(t, chain.Store(block0, &emptyCommitments, stateUpdate0, nil)) + + chain = blockchain.New(testDB, &utils.Mainnet) + syncer := &fakeSyncer{newHeads: feed.New[*core.Header]()} + handler := rpc.New(chain, syncer, nil, "", utils.NewNopZapLogger()) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + go func() { + require.NoError(t, handler.Run(ctx)) + }() + // Technically, there's a race between goroutine above and the SubscribeNewHeads call down below. + // Sleep for a moment just in case. + time.Sleep(50 * time.Millisecond) + + serverConn, clientConn := net.Pipe() + t.Cleanup(func() { + require.NoError(t, serverConn.Close()) + require.NoError(t, clientConn.Close()) + }) + + subCtx := context.WithValue(ctx, jsonrpc.ConnKey{}, &fakeConn{w: serverConn}) + + // Subscribe to a block that doesn't exist. + id, rpcErr := handler.SubscribeNewHeads(subCtx, &rpc.BlockID{Number: 1025}) + require.Equal(t, rpc.ErrBlockNotFound, rpcErr) + require.Zero(t, id) + + // Subscribe to a block that exists. + id, rpcErr = handler.SubscribeNewHeads(subCtx, &rpc.BlockID{Number: 0}) + require.Nil(t, rpcErr) + require.NotZero(t, id) + + // Check block 0 content + want := `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x47c3637b57c2b079b93c61539950c17e868a28f46cdef28f88521067f21e943","parent_hash":"0x0","block_number":0,"new_root":"0x21870ba80540e7831fb21c591ee93481f5ae1bb71ff85a86ddd465be4eddee6","timestamp":1637069048,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` + want = fmt.Sprintf(want, id.ID) + got := make([]byte, len(want)) + _, err = clientConn.Read(got) + require.NoError(t, err) + require.Equal(t, want, string(got)) + + // Simulate a new block + syncer.newHeads.Send(&core.Header{ + Hash: utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), + ParentHash: utils.HexToFelt(t, "0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb"), + Number: 2, + GlobalStateRoot: utils.HexToFelt(t, "0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9"), + Timestamp: 1637084470, + SequencerAddress: utils.HexToFelt(t, "0x0"), + L1DataGasPrice: &core.GasPrice{ + PriceInFri: utils.HexToFelt(t, "0x0"), + PriceInWei: utils.HexToFelt(t, "0x0"), + }, + GasPrice: utils.HexToFelt(t, "0x0"), + GasPriceSTRK: utils.HexToFelt(t, "0x0"), + L1DAMode: core.Calldata, + ProtocolVersion: "", + }) + + // Check new block content + want = `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` + want = fmt.Sprintf(want, id.ID) + got = make([]byte, len(want)) + _, err = clientConn.Read(got) + require.NoError(t, err) + require.Equal(t, want, string(got)) +} From 8dc7e093da14c93ebc071740afc299bffb9483fc Mon Sep 17 00:00:00 2001 From: weiihann Date: Mon, 14 Oct 2024 18:41:55 +0800 Subject: [PATCH 05/26] fix golint --- rpc/events_test.go | 72 ++++++++++++++++------------------------------ 1 file changed, 25 insertions(+), 47 deletions(-) diff --git a/rpc/events_test.go b/rpc/events_test.go index d859216275..b35280304c 100644 --- a/rpc/events_test.go +++ b/rpc/events_test.go @@ -27,6 +27,10 @@ import ( var emptyCommitments = core.BlockCommitments{} +const ( + testResponse = `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` +) + func TestEvents(t *testing.T) { var pendingB *core.Block pendingBlockFn := func() *core.Block { @@ -284,26 +288,10 @@ func TestSubscribeNewHeadsAndUnsubscribe(t *testing.T) { require.Nil(t, rpcErr) // Simulate a new block - syncer.newHeads.Send(&core.Header{ - Hash: utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), - ParentHash: utils.HexToFelt(t, "0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb"), - Number: 2, - GlobalStateRoot: utils.HexToFelt(t, "0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9"), - Timestamp: 1637084470, - SequencerAddress: utils.HexToFelt(t, "0x0"), - L1DataGasPrice: &core.GasPrice{ - PriceInFri: utils.HexToFelt(t, "0x0"), - PriceInWei: utils.HexToFelt(t, "0x0"), - }, - GasPrice: utils.HexToFelt(t, "0x0"), - GasPriceSTRK: utils.HexToFelt(t, "0x0"), - L1DAMode: core.Calldata, - ProtocolVersion: "", - }) + syncer.newHeads.Send(testHeader(t)) // Receive a block header. - want := `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` - want = fmt.Sprintf(want, id.ID) + want := fmt.Sprintf(testResponse, id.ID) got := make([]byte, len(want)) _, err := clientConn.Read(got) require.NoError(t, err) @@ -388,30 +376,14 @@ func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { require.Equal(t, secondWant, string(secondGot)) // Simulate a new block - syncer.newHeads.Send(&core.Header{ - Hash: utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), - ParentHash: utils.HexToFelt(t, "0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb"), - Number: 2, - GlobalStateRoot: utils.HexToFelt(t, "0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9"), - Timestamp: 1637084470, - SequencerAddress: utils.HexToFelt(t, "0x0"), - L1DataGasPrice: &core.GasPrice{ - PriceInFri: utils.HexToFelt(t, "0x0"), - PriceInWei: utils.HexToFelt(t, "0x0"), - }, - GasPrice: utils.HexToFelt(t, "0x0"), - GasPriceSTRK: utils.HexToFelt(t, "0x0"), - L1DAMode: core.Calldata, - ProtocolVersion: "", - }) + syncer.newHeads.Send(testHeader(t)) // Receive a block header. - want = `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` - firstWant = fmt.Sprintf(want, firstID) + firstWant = fmt.Sprintf(testResponse, firstID) _, firstGot, err = conn1.Read(ctx) require.NoError(t, err) require.Equal(t, firstWant, string(firstGot)) - secondWant = fmt.Sprintf(want, secondID) + secondWant = fmt.Sprintf(testResponse, secondID) _, secondGot, err = conn2.Read(ctx) require.NoError(t, err) require.Equal(t, secondWant, string(secondGot)) @@ -477,7 +449,20 @@ func TestSubscribeNewHeadsHistorical(t *testing.T) { require.Equal(t, want, string(got)) // Simulate a new block - syncer.newHeads.Send(&core.Header{ + syncer.newHeads.Send(testHeader(t)) + + // Check new block content + want = fmt.Sprintf(testResponse, id.ID) + got = make([]byte, len(want)) + _, err = clientConn.Read(got) + require.NoError(t, err) + require.Equal(t, want, string(got)) +} + +func testHeader(t *testing.T) *core.Header { + t.Helper() + + header := &core.Header{ Hash: utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), ParentHash: utils.HexToFelt(t, "0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb"), Number: 2, @@ -492,13 +477,6 @@ func TestSubscribeNewHeadsHistorical(t *testing.T) { GasPriceSTRK: utils.HexToFelt(t, "0x0"), L1DAMode: core.Calldata, ProtocolVersion: "", - }) - - // Check new block content - want = `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` - want = fmt.Sprintf(want, id.ID) - got = make([]byte, len(want)) - _, err = clientConn.Read(got) - require.NoError(t, err) - require.Equal(t, want, string(got)) + } + return header } From 6e11401ab0afaae4780181ad781ec11b129d6b77 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 15 Oct 2024 17:39:43 +0800 Subject: [PATCH 06/26] Implement starknet_subscriptionReorg --- mocks/mock_synchronizer.go | 14 +++++ rpc/events.go | 49 ++++++++++++++- rpc/events_test.go | 73 ++++++++++++++++++--- rpc/handlers.go | 8 ++- sync/sync.go | 126 +++++++++++++------------------------ sync/sync_test.go | 14 ++++- 6 files changed, 187 insertions(+), 97 deletions(-) diff --git a/mocks/mock_synchronizer.go b/mocks/mock_synchronizer.go index 910e5007e6..ac325f577f 100644 --- a/mocks/mock_synchronizer.go +++ b/mocks/mock_synchronizer.go @@ -127,3 +127,17 @@ func (mr *MockSyncReaderMockRecorder) SubscribeNewHeads() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeNewHeads", reflect.TypeOf((*MockSyncReader)(nil).SubscribeNewHeads)) } + +// SubscribeReorg mocks base method. +func (m *MockSyncReader) SubscribeReorg() sync.ReorgSubscription { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubscribeReorg") + ret0, _ := ret[0].(sync.ReorgSubscription) + return ret0 +} + +// SubscribeReorg indicates an expected call of SubscribeReorg. +func (mr *MockSyncReaderMockRecorder) SubscribeReorg() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeReorg", reflect.TypeOf((*MockSyncReader)(nil).SubscribeReorg)) +} diff --git a/rpc/events.go b/rpc/events.go index ec053b546a..b4f3d291e0 100644 --- a/rpc/events.go +++ b/rpc/events.go @@ -9,6 +9,8 @@ import ( "github.com/NethermindEth/juno/core/felt" "github.com/NethermindEth/juno/feed" "github.com/NethermindEth/juno/jsonrpc" + "github.com/NethermindEth/juno/sync" + "github.com/sourcegraph/conc" ) const ( @@ -80,15 +82,18 @@ func (h *Handler) SubscribeNewHeads(ctx context.Context, blockID *BlockID) (*Sub h.mu.Unlock() headerSub := h.newHeads.Subscribe() + reorgSub := h.reorgs.Subscribe() // as per the spec, reorgs are also sent in the new heads subscription sub.wg.Go(func() { defer func() { h.unsubscribe(sub, id) headerSub.Unsubscribe() + reorgSub.Unsubscribe() }() - newHeadersChan := make(chan *core.Header, MaxBlocksBack) + var wg conc.WaitGroup - sub.wg.Go(func() { + newHeadersChan := make(chan *core.Header, MaxBlocksBack) + wg.Go(func() { h.bufferNewHeaders(subscriptionCtx, headerSub, newHeadersChan) }) @@ -97,7 +102,15 @@ func (h *Handler) SubscribeNewHeads(ctx context.Context, blockID *BlockID) (*Sub return } - h.processNewHeaders(subscriptionCtx, newHeadersChan, w, id) + wg.Go(func() { + h.processNewHeaders(subscriptionCtx, newHeadersChan, w, id) + }) + + wg.Go(func() { + h.processReorgs(subscriptionCtx, reorgSub, w, id) + }) + + wg.Wait() }) return &SubscriptionID{ID: id}, nil @@ -204,6 +217,36 @@ func (h *Handler) sendHeader(w jsonrpc.Conn, header *core.Header, id uint64) err return err } +func (h *Handler) processReorgs(ctx context.Context, reorgSub *feed.Subscription[*sync.ReorgData], w jsonrpc.Conn, id uint64) { + for { + select { + case <-ctx.Done(): + return + case reorg := <-reorgSub.Recv(): + if err := h.sendReorg(w, reorg, id); err != nil { + h.log.Warnw("Error sending reorg", "err", err) + return + } + } + } +} + +func (h *Handler) sendReorg(w jsonrpc.Conn, reorg *sync.ReorgData, id uint64) error { + resp, err := json.Marshal(jsonrpc.Request{ + Version: "2.0", + Method: "starknet_subscriptionReorg", + Params: map[string]any{ + "subscription_id": id, + "result": reorg, + }, + }) + if err != nil { + return err + } + _, err = w.Write(resp) + return err +} + func (h *Handler) Unsubscribe(ctx context.Context, id uint64) (bool, *jsonrpc.Error) { w, ok := jsonrpc.ConnFromContext(ctx) if !ok { diff --git a/rpc/events_test.go b/rpc/events_test.go index b35280304c..757153053d 100644 --- a/rpc/events_test.go +++ b/rpc/events_test.go @@ -28,7 +28,7 @@ import ( var emptyCommitments = core.BlockCommitments{} const ( - testResponse = `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` + newHeadsResponse = `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` ) func TestEvents(t *testing.T) { @@ -240,12 +240,24 @@ func (fc *fakeConn) Equal(other jsonrpc.Conn) bool { type fakeSyncer struct { newHeads *feed.Feed[*core.Header] + reorgs *feed.Feed[*sync.ReorgData] +} + +func newFakeSyncer() *fakeSyncer { + return &fakeSyncer{ + newHeads: feed.New[*core.Header](), + reorgs: feed.New[*sync.ReorgData](), + } } func (fs *fakeSyncer) SubscribeNewHeads() sync.HeaderSubscription { return sync.HeaderSubscription{Subscription: fs.newHeads.Subscribe()} } +func (fs *fakeSyncer) SubscribeReorg() sync.ReorgSubscription { + return sync.ReorgSubscription{Subscription: fs.reorgs.Subscribe()} +} + func (fs *fakeSyncer) StartingBlockNumber() (uint64, error) { return 0, nil } @@ -258,7 +270,7 @@ func TestSubscribeNewHeadsAndUnsubscribe(t *testing.T) { t.Parallel() chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) - syncer := &fakeSyncer{newHeads: feed.New[*core.Header]()} + syncer := newFakeSyncer() handler := rpc.New(chain, syncer, nil, "", utils.NewNopZapLogger()) ctx, cancel := context.WithCancel(context.Background()) @@ -291,7 +303,7 @@ func TestSubscribeNewHeadsAndUnsubscribe(t *testing.T) { syncer.newHeads.Send(testHeader(t)) // Receive a block header. - want := fmt.Sprintf(testResponse, id.ID) + want := fmt.Sprintf(newHeadsResponse, id.ID) got := make([]byte, len(want)) _, err := clientConn.Read(got) require.NoError(t, err) @@ -325,7 +337,7 @@ func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { log := utils.NewNopZapLogger() chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) - syncer := &fakeSyncer{newHeads: feed.New[*core.Header]()} + syncer := newFakeSyncer() handler := rpc.New(chain, syncer, nil, "", log) ctx, cancel := context.WithCancel(context.Background()) @@ -379,11 +391,11 @@ func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { syncer.newHeads.Send(testHeader(t)) // Receive a block header. - firstWant = fmt.Sprintf(testResponse, firstID) + firstWant = fmt.Sprintf(newHeadsResponse, firstID) _, firstGot, err = conn1.Read(ctx) require.NoError(t, err) require.Equal(t, firstWant, string(firstGot)) - secondWant = fmt.Sprintf(testResponse, secondID) + secondWant = fmt.Sprintf(newHeadsResponse, secondID) _, secondGot, err = conn2.Read(ctx) require.NoError(t, err) require.Equal(t, secondWant, string(secondGot)) @@ -409,7 +421,7 @@ func TestSubscribeNewHeadsHistorical(t *testing.T) { assert.NoError(t, chain.Store(block0, &emptyCommitments, stateUpdate0, nil)) chain = blockchain.New(testDB, &utils.Mainnet) - syncer := &fakeSyncer{newHeads: feed.New[*core.Header]()} + syncer := newFakeSyncer() handler := rpc.New(chain, syncer, nil, "", utils.NewNopZapLogger()) ctx, cancel := context.WithCancel(context.Background()) @@ -452,7 +464,7 @@ func TestSubscribeNewHeadsHistorical(t *testing.T) { syncer.newHeads.Send(testHeader(t)) // Check new block content - want = fmt.Sprintf(testResponse, id.ID) + want = fmt.Sprintf(newHeadsResponse, id.ID) got = make([]byte, len(want)) _, err = clientConn.Read(got) require.NoError(t, err) @@ -480,3 +492,48 @@ func testHeader(t *testing.T) *core.Header { } return header } + +func TestSubscriptionReorg(t *testing.T) { + t.Parallel() + + chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) + syncer := newFakeSyncer() + handler := rpc.New(chain, syncer, nil, "", utils.NewNopZapLogger()) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + go func() { + require.NoError(t, handler.Run(ctx)) + }() + time.Sleep(50 * time.Millisecond) + + serverConn, clientConn := net.Pipe() + t.Cleanup(func() { + require.NoError(t, serverConn.Close()) + require.NoError(t, clientConn.Close()) + }) + + subCtx := context.WithValue(ctx, jsonrpc.ConnKey{}, &fakeConn{w: serverConn}) + + // Subscribe to new heads which will send a + id, rpcErr := handler.SubscribeNewHeads(subCtx, nil) + require.Nil(t, rpcErr) + require.NotZero(t, id) + + // Simulate a reorg + syncer.reorgs.Send(&sync.ReorgData{ + StartBlockHash: utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), + StartBlockNum: 0, + EndBlockHash: utils.HexToFelt(t, "0x34e815552e42c5eb5233b99de2d3d7fd396e575df2719bf98e7ed2794494f86"), + EndBlockNum: 2, + }) + + // Receive reorg event + want := `{"jsonrpc":"2.0","method":"starknet_subscriptionReorg","params":{"result":{"starting_block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","starting_block_number":0,"ending_block_hash":"0x34e815552e42c5eb5233b99de2d3d7fd396e575df2719bf98e7ed2794494f86","ending_block_number":2},"subscription_id":%d}}` + want = fmt.Sprintf(want, id.ID) + got := make([]byte, len(want)) + _, err := clientConn.Read(got) + require.NoError(t, err) + require.Equal(t, want, string(got)) +} diff --git a/rpc/handlers.go b/rpc/handlers.go index 79e4d86686..d3ef4d8144 100644 --- a/rpc/handlers.go +++ b/rpc/handlers.go @@ -97,6 +97,7 @@ type Handler struct { version string newHeads *feed.Feed[*core.Header] + reorgs *feed.Feed[*sync.ReorgData] idgen func() uint64 mu stdsync.Mutex // protects subscriptions. @@ -137,6 +138,7 @@ func New(bcReader blockchain.Reader, syncReader sync.Reader, virtualMachine vm.V }, version: version, newHeads: feed.New[*core.Header](), + reorgs: feed.New[*sync.ReorgData](), subscriptions: make(map[uint64]*subscription), blockTraceCache: lru.NewCache[traceCacheKey, []TracedBlockTransaction](traceCacheSize), @@ -178,8 +180,12 @@ func (h *Handler) WithGateway(gatewayClient Gateway) *Handler { func (h *Handler) Run(ctx context.Context) error { newHeadsSub := h.syncReader.SubscribeNewHeads().Subscription + reorgsSub := h.syncReader.SubscribeReorg().Subscription defer newHeadsSub.Unsubscribe() - feed.Tee[*core.Header](newHeadsSub, h.newHeads) + defer reorgsSub.Unsubscribe() + feed.Tee(newHeadsSub, h.newHeads) + feed.Tee(reorgsSub, h.reorgs) + <-ctx.Done() for _, sub := range h.subscriptions { sub.wg.Wait() diff --git a/sync/sync.go b/sync/sync.go index c268ffa7e3..289fa658d9 100644 --- a/sync/sync.go +++ b/sync/sync.go @@ -38,6 +38,10 @@ type HeaderSubscription struct { *feed.Subscription[*core.Header] } +type ReorgSubscription struct { + *feed.Subscription[*ReorgData] +} + // Todo: Since this is also going to be implemented by p2p package we should move this interface to node package // //go:generate mockgen -destination=../mocks/mock_synchronizer.go -package=mocks -mock_names Reader=MockSyncReader github.com/NethermindEth/juno/sync Reader @@ -45,10 +49,7 @@ type Reader interface { StartingBlockNumber() (uint64, error) HighestBlockHeader() *core.Header SubscribeNewHeads() HeaderSubscription - - Pending() (*Pending, error) - PendingBlock() *core.Block - PendingState() (core.StateReader, func() error, error) + SubscribeReorg() ReorgSubscription } // This is temporary and will be removed once the p2p synchronizer implements this interface. @@ -66,16 +67,20 @@ func (n *NoopSynchronizer) SubscribeNewHeads() HeaderSubscription { return HeaderSubscription{feed.New[*core.Header]().Subscribe()} } -func (n *NoopSynchronizer) PendingBlock() *core.Block { - return nil -} - -func (n *NoopSynchronizer) Pending() (*Pending, error) { - return nil, errors.New("Pending() is not implemented") +func (n *NoopSynchronizer) SubscribeReorg() ReorgSubscription { + return ReorgSubscription{feed.New[*ReorgData]().Subscribe()} } -func (n *NoopSynchronizer) PendingState() (core.StateReader, func() error, error) { - return nil, nil, errors.New("PendingState() not implemented") +// ReorgData represents data about reorganised blocks, starting and ending block number and hash +type ReorgData struct { + // StartBlockHash is the hash of the first known block of the orphaned chain + StartBlockHash *felt.Felt `json:"starting_block_hash"` + // StartBlockNum is the number of the first known block of the orphaned chain + StartBlockNum uint64 `json:"starting_block_number"` + // The last known block of the orphaned chain + EndBlockHash *felt.Felt `json:"ending_block_hash"` + // Number of the last known block of the orphaned chain + EndBlockNum uint64 `json:"ending_block_number"` } // Synchronizer manages a list of StarknetData to fetch the latest blockchain updates @@ -87,6 +92,7 @@ type Synchronizer struct { startingBlockNumber *uint64 highestBlockHeader atomic.Pointer[core.Header] newHeads *feed.Feed[*core.Header] + reorgFeed *feed.Feed[*ReorgData] log utils.SimpleLogger listener EventListener @@ -95,6 +101,8 @@ type Synchronizer struct { pendingPollInterval time.Duration catchUpMode bool plugin junoplugin.JunoPlugin + + currReorg *ReorgData // If nil, no reorg is happening } func New(bc *blockchain.Blockchain, starkNetData starknetdata.StarknetData, log utils.SimpleLogger, @@ -106,6 +114,7 @@ func New(bc *blockchain.Blockchain, starkNetData starknetdata.StarknetData, log starknetData: starkNetData, log: log, newHeads: feed.New[*core.Header](), + reorgFeed: feed.New[*ReorgData](), pendingPollInterval: pendingPollInterval, listener: &SelectiveListener{}, readOnlyBlockchain: readOnlyBlockchain, @@ -304,6 +313,11 @@ func (s *Synchronizer) verifierTask(ctx context.Context, block *core.Block, stat s.highestBlockHeader.CompareAndSwap(highestBlockHeader, block.Header) } + if s.currReorg != nil { + s.reorgFeed.Send(s.currReorg) + s.currReorg = nil // reset the reorg data + } + s.newHeads.Send(block.Header) s.log.Infow("Stored Block", "number", block.Number, "hash", block.Hash.ShortString(), "root", block.GlobalStateRoot.ShortString()) @@ -403,6 +417,19 @@ func (s *Synchronizer) revertHead(forkBlock *core.Block) { } else { s.log.Infow("Reverted HEAD", "reverted", localHead) } + + if s.currReorg == nil { // first block of the reorg + s.currReorg = &ReorgData{ + StartBlockHash: localHead, + StartBlockNum: head.Number, + EndBlockHash: localHead, + EndBlockNum: head.Number, + } + } else { // not the first block of the reorg, adjust the starting block + s.currReorg.StartBlockHash = localHead + s.currReorg.StartBlockNum = head.Number + } + s.listener.OnReorg(head.Number) } @@ -519,77 +546,8 @@ func (s *Synchronizer) SubscribeNewHeads() HeaderSubscription { } } -// StorePending stores a pending block given that it is for the next height -func (s *Synchronizer) StorePending(p *Pending) error { - err := blockchain.CheckBlockVersion(p.Block.ProtocolVersion) - if err != nil { - return err - } - - expectedParentHash := new(felt.Felt) - h, err := s.blockchain.HeadsHeader() - if err != nil && !errors.Is(err, db.ErrKeyNotFound) { - return err - } else if err == nil { - expectedParentHash = h.Hash - } - - if !expectedParentHash.Equal(p.Block.ParentHash) { - return fmt.Errorf("store pending: %w", blockchain.ErrParentDoesNotMatchHead) - } - - if existingPending, err := s.Pending(); err == nil { - if existingPending.Block.TransactionCount >= p.Block.TransactionCount { - // ignore the incoming pending if it has fewer transactions than the one we already have - return nil - } - } else if !errors.Is(err, ErrPendingBlockNotFound) { - return err - } - s.pending.Store(p) - - return nil -} - -func (s *Synchronizer) Pending() (*Pending, error) { - p := s.pending.Load() - if p == nil { - return nil, ErrPendingBlockNotFound - } - - expectedParentHash := &felt.Zero - if head, err := s.blockchain.HeadsHeader(); err == nil { - expectedParentHash = head.Hash - } - if p.Block.ParentHash.Equal(expectedParentHash) { - return p, nil - } - - // Since the pending block in the cache is outdated remove it - s.pending.Store(nil) - - return nil, ErrPendingBlockNotFound -} - -func (s *Synchronizer) PendingBlock() *core.Block { - pending, err := s.Pending() - if err != nil { - return nil - } - return pending.Block -} - -// PendingState returns the state resulting from execution of the pending block -func (s *Synchronizer) PendingState() (core.StateReader, func() error, error) { - txn, err := s.db.NewTransaction(false) - if err != nil { - return nil, nil, err - } - - pending, err := s.Pending() - if err != nil { - return nil, nil, utils.RunAndWrapOnError(txn.Discard, err) +func (s *Synchronizer) SubscribeReorg() ReorgSubscription { + return ReorgSubscription{ + Subscription: s.reorgFeed.Subscribe(), } - - return NewPendingState(pending.StateUpdate.StateDiff, pending.NewClasses, core.NewState(txn)), txn.Discard, nil } diff --git a/sync/sync_test.go b/sync/sync_test.go index ab97fc322b..ffe8474baf 100644 --- a/sync/sync_test.go +++ b/sync/sync_test.go @@ -160,8 +160,12 @@ func TestReorg(t *testing.T) { head, err := bc.HeadsHeader() require.NoError(t, err) require.Equal(t, utils.HexToFelt(t, "0x34e815552e42c5eb5233b99de2d3d7fd396e575df2719bf98e7ed2794494f86"), head.Hash) + integEnd := head + integStart, err := bc.BlockHeaderByNumber(0) + require.NoError(t, err) - synchronizer = sync.New(bc, mainGw, utils.NewNopZapLogger(), 0, false, testDB) + synchronizer = sync.New(bc, mainGw, utils.NewNopZapLogger(), 0, false) + sub := synchronizer.SubscribeReorg() ctx, cancel = context.WithTimeout(context.Background(), timeout) require.NoError(t, synchronizer.Run(ctx)) cancel() @@ -170,6 +174,14 @@ func TestReorg(t *testing.T) { head, err = bc.HeadsHeader() require.NoError(t, err) require.Equal(t, utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), head.Hash) + + // Validate reorg event + got, ok := <-sub.Recv() + require.True(t, ok) + assert.Equal(t, integEnd.Hash, got.EndBlockHash) + assert.Equal(t, integEnd.Number, got.EndBlockNum) + assert.Equal(t, integStart.Hash, got.StartBlockHash) + assert.Equal(t, integStart.Number, got.StartBlockNum) }) } From 6680c295df509bf855c23fea7e449a722343e86e Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 22 Oct 2024 00:19:20 +0800 Subject: [PATCH 07/26] starknet_subscribePendingTransactions all tests pass --- mocks/mock_synchronizer.go | 14 ++ rpc/events.go | 127 ++++++++++- rpc/events_test.go | 439 ++++++++++++++++++++++++++----------- rpc/handlers.go | 20 +- sync/sync.go | 20 ++ sync/sync_test.go | 97 +------- 6 files changed, 501 insertions(+), 216 deletions(-) diff --git a/mocks/mock_synchronizer.go b/mocks/mock_synchronizer.go index ac325f577f..d04a733db0 100644 --- a/mocks/mock_synchronizer.go +++ b/mocks/mock_synchronizer.go @@ -128,6 +128,20 @@ func (mr *MockSyncReaderMockRecorder) SubscribeNewHeads() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeNewHeads", reflect.TypeOf((*MockSyncReader)(nil).SubscribeNewHeads)) } +// SubscribePendingTxs mocks base method. +func (m *MockSyncReader) SubscribePendingTxs() sync.PendingTxSubscription { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubscribePendingTxs") + ret0, _ := ret[0].(sync.PendingTxSubscription) + return ret0 +} + +// SubscribePendingTxs indicates an expected call of SubscribePendingTxs. +func (mr *MockSyncReaderMockRecorder) SubscribePendingTxs() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribePendingTxs", reflect.TypeOf((*MockSyncReader)(nil).SubscribePendingTxs)) +} + // SubscribeReorg mocks base method. func (m *MockSyncReader) SubscribeReorg() sync.ReorgSubscription { m.ctrl.T.Helper() diff --git a/rpc/events.go b/rpc/events.go index b4f3d291e0..6af3c363d0 100644 --- a/rpc/events.go +++ b/rpc/events.go @@ -14,7 +14,8 @@ import ( ) const ( - MaxBlocksBack = 1024 + MaxBlocksBack = 1024 + MaxAddressesInFilter = 1000 // TODO(weiihann): not finalised yet ) type EventsArg struct { @@ -116,6 +117,130 @@ func (h *Handler) SubscribeNewHeads(ctx context.Context, blockID *BlockID) (*Sub return &SubscriptionID{ID: id}, nil } +func (h *Handler) SubscribePendingTxs(ctx context.Context, getDetails *bool, senderAddr []felt.Felt) (*SubscriptionID, *jsonrpc.Error) { + w, ok := jsonrpc.ConnFromContext(ctx) + if !ok { + return nil, jsonrpc.Err(jsonrpc.MethodNotFound, nil) + } + + if len(senderAddr) > MaxAddressesInFilter { + return nil, ErrTooManyAddressesInFilter + } + + id := h.idgen() + subscriptionCtx, subscriptionCtxCancel := context.WithCancel(ctx) + sub := &subscription{ + cancel: subscriptionCtxCancel, + conn: w, + } + h.mu.Lock() + h.subscriptions[id] = sub + h.mu.Unlock() + + pendingTxsSub := h.pendingTxs.Subscribe() + sub.wg.Go(func() { + defer func() { + h.unsubscribe(sub, id) + pendingTxsSub.Unsubscribe() + }() + + h.processPendingTxs(subscriptionCtx, getDetails != nil && *getDetails, senderAddr, pendingTxsSub, w, id) + }) + + return &SubscriptionID{ID: id}, nil +} + +func (h *Handler) processPendingTxs( + ctx context.Context, + getDetails bool, + senderAddr []felt.Felt, + pendingTxsSub *feed.Subscription[[]core.Transaction], + w jsonrpc.Conn, + id uint64, +) { + for { + select { + case <-ctx.Done(): + return + case pendingTxs := <-pendingTxsSub.Recv(): + filteredTxs := h.filterTxs(pendingTxs, getDetails, senderAddr) + if err := h.sendPendingTxs(w, filteredTxs, id); err != nil { + h.log.Warnw("Error sending pending transactions", "err", err) + return + } + } + } +} + +func (h *Handler) filterTxs(pendingTxs []core.Transaction, getDetails bool, senderAddr []felt.Felt) interface{} { + if getDetails { + return h.filterTxDetails(pendingTxs, senderAddr) + } + return h.filterTxHashes(pendingTxs, senderAddr) +} + +func (h *Handler) filterTxDetails(pendingTxs []core.Transaction, senderAddr []felt.Felt) []*Transaction { + filteredTxs := make([]*Transaction, 0, len(pendingTxs)) + for _, txn := range pendingTxs { + if h.shouldIncludeTx(txn, senderAddr) { + filteredTxs = append(filteredTxs, AdaptTransaction(txn)) + } + } + return filteredTxs +} + +func (h *Handler) filterTxHashes(pendingTxs []core.Transaction, senderAddr []felt.Felt) []felt.Felt { + filteredTxHashes := make([]felt.Felt, 0, len(pendingTxs)) + for _, txn := range pendingTxs { + if h.shouldIncludeTx(txn, senderAddr) { + filteredTxHashes = append(filteredTxHashes, *txn.Hash()) + } + } + return filteredTxHashes +} + +func (h *Handler) shouldIncludeTx(txn core.Transaction, senderAddr []felt.Felt) bool { + if len(senderAddr) == 0 { + return true + } + + // + switch t := txn.(type) { + case *core.InvokeTransaction: + for _, addr := range senderAddr { + if t.SenderAddress.Equal(&addr) { + return true + } + } + case *core.DeclareTransaction: + for _, addr := range senderAddr { + if t.SenderAddress.Equal(&addr) { + return true + } + } + } + + return false +} + +func (h *Handler) sendPendingTxs(w jsonrpc.Conn, result interface{}, id uint64) error { + req := jsonrpc.Request{ + Version: "2.0", + Method: "starknet_subscriptionPendingTransactions", + Params: map[string]interface{}{ + "subscription_id": id, + "result": result, + }, + } + + resp, err := json.Marshal(req) + if err != nil { + return err + } + _, err = w.Write(resp) + return err +} + // getStartAndLatestHeaders gets the start and latest header for the subscription func (h *Handler) getStartAndLatestHeaders(blockID *BlockID) (*core.Header, *core.Header, *jsonrpc.Error) { if blockID == nil || blockID.Latest { diff --git a/rpc/events_test.go b/rpc/events_test.go index 757153053d..73c8f45dbd 100644 --- a/rpc/events_test.go +++ b/rpc/events_test.go @@ -3,8 +3,6 @@ package rpc_test import ( "context" "fmt" - "io" - "net" "net/http/httptest" "testing" "time" @@ -28,7 +26,8 @@ import ( var emptyCommitments = core.BlockCommitments{} const ( - newHeadsResponse = `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` + newHeadsResponse = `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` + subscribeResponse = `{"jsonrpc":"2.0","result":{"subscription_id":%d},"id":1}` ) func TestEvents(t *testing.T) { @@ -222,31 +221,17 @@ func TestEvents(t *testing.T) { }) } -type fakeConn struct { - w io.Writer -} - -func (fc *fakeConn) Write(p []byte) (int, error) { - return fc.w.Write(p) -} - -func (fc *fakeConn) Equal(other jsonrpc.Conn) bool { - fc2, ok := other.(*fakeConn) - if !ok { - return false - } - return fc.w == fc2.w -} - type fakeSyncer struct { - newHeads *feed.Feed[*core.Header] - reorgs *feed.Feed[*sync.ReorgData] + newHeads *feed.Feed[*core.Header] + reorgs *feed.Feed[*sync.ReorgData] + pendingTxs *feed.Feed[[]core.Transaction] } func newFakeSyncer() *fakeSyncer { return &fakeSyncer{ - newHeads: feed.New[*core.Header](), - reorgs: feed.New[*sync.ReorgData](), + newHeads: feed.New[*core.Header](), + reorgs: feed.New[*sync.ReorgData](), + pendingTxs: feed.New[[]core.Transaction](), } } @@ -258,6 +243,10 @@ func (fs *fakeSyncer) SubscribeReorg() sync.ReorgSubscription { return sync.ReorgSubscription{Subscription: fs.reorgs.Subscribe()} } +func (fs *fakeSyncer) SubscribePendingTxs() sync.PendingTxSubscription { + return sync.PendingTxSubscription{Subscription: fs.pendingTxs.Subscribe()} +} + func (fs *fakeSyncer) StartingBlockNumber() (uint64, error) { return 0, nil } @@ -266,9 +255,10 @@ func (fs *fakeSyncer) HighestBlockHeader() *core.Header { return nil } -func TestSubscribeNewHeadsAndUnsubscribe(t *testing.T) { +func TestSubscribeNewHeads(t *testing.T) { t.Parallel() + log := utils.NewNopZapLogger() chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) syncer := newFakeSyncer() handler := rpc.New(chain, syncer, nil, "", utils.NewNopZapLogger()) @@ -283,53 +273,37 @@ func TestSubscribeNewHeadsAndUnsubscribe(t *testing.T) { // Sleep for a moment just in case. time.Sleep(50 * time.Millisecond) - serverConn, clientConn := net.Pipe() - t.Cleanup(func() { - require.NoError(t, serverConn.Close()) - require.NoError(t, clientConn.Close()) - }) + server := jsonrpc.NewServer(1, log) + require.NoError(t, server.RegisterMethods(jsonrpc.Method{ + Name: "starknet_subscribeNewHeads", + Params: []jsonrpc.Parameter{{Name: "block", Optional: true}}, + Handler: handler.SubscribeNewHeads, + })) + ws := jsonrpc.NewWebsocket(server, log) + httpSrv := httptest.NewServer(ws) + + conn, _, err := websocket.Dial(ctx, httpSrv.URL, nil) + require.NoError(t, err) - // Subscribe without setting the connection on the context. - id, rpcErr := handler.SubscribeNewHeads(ctx, nil) - require.Zero(t, id) - require.Equal(t, jsonrpc.MethodNotFound, rpcErr.Code) + id := uint64(1) + handler.WithIDGen(func() uint64 { return id }) - // Subscribe correctly. - subCtx := context.WithValue(ctx, jsonrpc.ConnKey{}, &fakeConn{w: serverConn}) - id, rpcErr = handler.SubscribeNewHeads(subCtx, nil) - require.Nil(t, rpcErr) + subscribeMsg := []byte(`{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads"}`) + require.NoError(t, conn.Write(ctx, websocket.MessageText, subscribeMsg)) + + want := fmt.Sprintf(subscribeResponse, id) + _, got, err := conn.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(got)) // Simulate a new block syncer.newHeads.Send(testHeader(t)) // Receive a block header. - want := fmt.Sprintf(newHeadsResponse, id.ID) - got := make([]byte, len(want)) - _, err := clientConn.Read(got) + want = fmt.Sprintf(newHeadsResponse, id) + _, headerGot, err := conn.Read(ctx) require.NoError(t, err) - require.Equal(t, want, string(got)) - - // Unsubscribe without setting the connection on the context. - ok, rpcErr := handler.Unsubscribe(ctx, id.ID) - require.Equal(t, jsonrpc.MethodNotFound, rpcErr.Code) - require.False(t, ok) - - // Unsubscribe on correct connection with the incorrect id. - ok, rpcErr = handler.Unsubscribe(subCtx, id.ID+1) - require.Equal(t, rpc.ErrSubscriptionNotFound, rpcErr) - require.False(t, ok) - - // Unsubscribe on incorrect connection with the correct id. - subCtx = context.WithValue(context.Background(), jsonrpc.ConnKey{}, &fakeConn{}) - ok, rpcErr = handler.Unsubscribe(subCtx, id.ID) - require.Equal(t, rpc.ErrSubscriptionNotFound, rpcErr) - require.False(t, ok) - - // Unsubscribe on correct connection with the correct id. - subCtx = context.WithValue(context.Background(), jsonrpc.ConnKey{}, &fakeConn{w: serverConn}) - ok, rpcErr = handler.Unsubscribe(subCtx, id.ID) - require.Nil(t, rpcErr) - require.True(t, ok) + require.Equal(t, want, string(headerGot)) } func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { @@ -374,15 +348,14 @@ func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { handler.WithIDGen(func() uint64 { return firstID }) require.NoError(t, conn1.Write(ctx, websocket.MessageText, subscribeMsg)) - want := `{"jsonrpc":"2.0","result":{"subscription_id":%d},"id":1}` - firstWant := fmt.Sprintf(want, firstID) + firstWant := fmt.Sprintf(subscribeResponse, firstID) _, firstGot, err := conn1.Read(ctx) require.NoError(t, err) require.Equal(t, firstWant, string(firstGot)) handler.WithIDGen(func() uint64 { return secondID }) require.NoError(t, conn2.Write(ctx, websocket.MessageText, subscribeMsg)) - secondWant := fmt.Sprintf(want, secondID) + secondWant := fmt.Sprintf(subscribeResponse, secondID) _, secondGot, err := conn2.Read(ctx) require.NoError(t, err) require.Equal(t, secondWant, string(secondGot)) @@ -407,6 +380,7 @@ func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { } func TestSubscribeNewHeadsHistorical(t *testing.T) { + log := utils.NewNopZapLogger() client := feeder.NewTestClient(t, &utils.Mainnet) gw := adaptfeeder.New(client) @@ -434,71 +408,53 @@ func TestSubscribeNewHeadsHistorical(t *testing.T) { // Sleep for a moment just in case. time.Sleep(50 * time.Millisecond) - serverConn, clientConn := net.Pipe() - t.Cleanup(func() { - require.NoError(t, serverConn.Close()) - require.NoError(t, clientConn.Close()) - }) + server := jsonrpc.NewServer(1, log) + require.NoError(t, server.RegisterMethods(jsonrpc.Method{ + Name: "starknet_subscribeNewHeads", + Params: []jsonrpc.Parameter{{Name: "block", Optional: true}}, + Handler: handler.SubscribeNewHeads, + })) + ws := jsonrpc.NewWebsocket(server, log) + httpSrv := httptest.NewServer(ws) - subCtx := context.WithValue(ctx, jsonrpc.ConnKey{}, &fakeConn{w: serverConn}) + conn, _, err := websocket.Dial(ctx, httpSrv.URL, nil) + require.NoError(t, err) - // Subscribe to a block that doesn't exist. - id, rpcErr := handler.SubscribeNewHeads(subCtx, &rpc.BlockID{Number: 1025}) - require.Equal(t, rpc.ErrBlockNotFound, rpcErr) - require.Zero(t, id) + id := uint64(1) + handler.WithIDGen(func() uint64 { return id }) - // Subscribe to a block that exists. - id, rpcErr = handler.SubscribeNewHeads(subCtx, &rpc.BlockID{Number: 0}) - require.Nil(t, rpcErr) - require.NotZero(t, id) + subscribeMsg := []byte(`{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads", "params":{"block":{"block_number":0}}}`) + require.NoError(t, conn.Write(ctx, websocket.MessageText, subscribeMsg)) - // Check block 0 content - want := `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x47c3637b57c2b079b93c61539950c17e868a28f46cdef28f88521067f21e943","parent_hash":"0x0","block_number":0,"new_root":"0x21870ba80540e7831fb21c591ee93481f5ae1bb71ff85a86ddd465be4eddee6","timestamp":1637069048,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` - want = fmt.Sprintf(want, id.ID) - got := make([]byte, len(want)) - _, err = clientConn.Read(got) + want := fmt.Sprintf(subscribeResponse, id) + _, got, err := conn.Read(ctx) require.NoError(t, err) require.Equal(t, want, string(got)) + // Check block 0 content + want = `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x47c3637b57c2b079b93c61539950c17e868a28f46cdef28f88521067f21e943","parent_hash":"0x0","block_number":0,"new_root":"0x21870ba80540e7831fb21c591ee93481f5ae1bb71ff85a86ddd465be4eddee6","timestamp":1637069048,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` + want = fmt.Sprintf(want, id) + _, block0Got, err := conn.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(block0Got)) + // Simulate a new block syncer.newHeads.Send(testHeader(t)) // Check new block content - want = fmt.Sprintf(newHeadsResponse, id.ID) - got = make([]byte, len(want)) - _, err = clientConn.Read(got) + want = fmt.Sprintf(newHeadsResponse, id) + _, newBlockGot, err := conn.Read(ctx) require.NoError(t, err) - require.Equal(t, want, string(got)) -} - -func testHeader(t *testing.T) *core.Header { - t.Helper() - - header := &core.Header{ - Hash: utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), - ParentHash: utils.HexToFelt(t, "0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb"), - Number: 2, - GlobalStateRoot: utils.HexToFelt(t, "0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9"), - Timestamp: 1637084470, - SequencerAddress: utils.HexToFelt(t, "0x0"), - L1DataGasPrice: &core.GasPrice{ - PriceInFri: utils.HexToFelt(t, "0x0"), - PriceInWei: utils.HexToFelt(t, "0x0"), - }, - GasPrice: utils.HexToFelt(t, "0x0"), - GasPriceSTRK: utils.HexToFelt(t, "0x0"), - L1DAMode: core.Calldata, - ProtocolVersion: "", - } - return header + require.Equal(t, want, string(newBlockGot)) } func TestSubscriptionReorg(t *testing.T) { t.Parallel() + log := utils.NewNopZapLogger() chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) syncer := newFakeSyncer() - handler := rpc.New(chain, syncer, nil, "", utils.NewNopZapLogger()) + handler := rpc.New(chain, syncer, nil, "", log) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) @@ -508,18 +464,28 @@ func TestSubscriptionReorg(t *testing.T) { }() time.Sleep(50 * time.Millisecond) - serverConn, clientConn := net.Pipe() - t.Cleanup(func() { - require.NoError(t, serverConn.Close()) - require.NoError(t, clientConn.Close()) - }) + server := jsonrpc.NewServer(1, log) + require.NoError(t, server.RegisterMethods(jsonrpc.Method{ + Name: "starknet_subscribeNewHeads", + Params: []jsonrpc.Parameter{{Name: "block", Optional: true}}, + Handler: handler.SubscribeNewHeads, + })) + ws := jsonrpc.NewWebsocket(server, log) + httpSrv := httptest.NewServer(ws) + + conn, _, err := websocket.Dial(ctx, httpSrv.URL, nil) + require.NoError(t, err) - subCtx := context.WithValue(ctx, jsonrpc.ConnKey{}, &fakeConn{w: serverConn}) + id := uint64(1) + handler.WithIDGen(func() uint64 { return id }) - // Subscribe to new heads which will send a - id, rpcErr := handler.SubscribeNewHeads(subCtx, nil) - require.Nil(t, rpcErr) - require.NotZero(t, id) + subscribeMsg := []byte(`{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads"}`) + require.NoError(t, conn.Write(ctx, websocket.MessageText, subscribeMsg)) + + want := fmt.Sprintf(subscribeResponse, id) + _, got, err := conn.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(got)) // Simulate a reorg syncer.reorgs.Send(&sync.ReorgData{ @@ -530,10 +496,231 @@ func TestSubscriptionReorg(t *testing.T) { }) // Receive reorg event - want := `{"jsonrpc":"2.0","method":"starknet_subscriptionReorg","params":{"result":{"starting_block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","starting_block_number":0,"ending_block_hash":"0x34e815552e42c5eb5233b99de2d3d7fd396e575df2719bf98e7ed2794494f86","ending_block_number":2},"subscription_id":%d}}` - want = fmt.Sprintf(want, id.ID) - got := make([]byte, len(want)) - _, err := clientConn.Read(got) + want = `{"jsonrpc":"2.0","method":"starknet_subscriptionReorg","params":{"result":{"starting_block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","starting_block_number":0,"ending_block_hash":"0x34e815552e42c5eb5233b99de2d3d7fd396e575df2719bf98e7ed2794494f86","ending_block_number":2},"subscription_id":%d}}` + want = fmt.Sprintf(want, id) + _, reorgGot, err := conn.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(reorgGot)) +} + +func TestSubscribePendingTxs(t *testing.T) { + t.Parallel() + + log := utils.NewNopZapLogger() + chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) + syncer := newFakeSyncer() + handler := rpc.New(chain, syncer, nil, "", utils.NewNopZapLogger()) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + go func() { + require.NoError(t, handler.Run(ctx)) + }() + time.Sleep(50 * time.Millisecond) + + server := jsonrpc.NewServer(1, log) + require.NoError(t, server.RegisterMethods(jsonrpc.Method{ + Name: "starknet_subscribePendingTransactions", + Params: []jsonrpc.Parameter{{Name: "transaction_details", Optional: true}, {Name: "sender_address", Optional: true}}, + Handler: handler.SubscribePendingTxs, + })) + ws := jsonrpc.NewWebsocket(server, log) + httpSrv := httptest.NewServer(ws) + + conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) + require.NoError(t, err) + + subscribeMsg := []byte(`{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions"}`) + + id := uint64(1) + handler.WithIDGen(func() uint64 { return id }) + require.NoError(t, conn1.Write(ctx, websocket.MessageText, subscribeMsg)) + + want := fmt.Sprintf(subscribeResponse, id) + _, got, err := conn1.Read(ctx) require.NoError(t, err) require.Equal(t, want, string(got)) + + hash1 := new(felt.Felt).SetUint64(1) + addr1 := new(felt.Felt).SetUint64(11) + + hash2 := new(felt.Felt).SetUint64(2) + addr2 := new(felt.Felt).SetUint64(22) + + hash3 := new(felt.Felt).SetUint64(3) + hash4 := new(felt.Felt).SetUint64(4) + hash5 := new(felt.Felt).SetUint64(5) + + syncer.pendingTxs.Send([]core.Transaction{ + &core.InvokeTransaction{TransactionHash: hash1, SenderAddress: addr1}, + &core.DeclareTransaction{TransactionHash: hash2, SenderAddress: addr2}, + &core.DeployTransaction{TransactionHash: hash3}, + &core.DeployAccountTransaction{DeployTransaction: core.DeployTransaction{TransactionHash: hash4}}, + &core.L1HandlerTransaction{TransactionHash: hash5}, + }) + + want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":["0x1","0x2","0x3","0x4","0x5"],"subscription_id":%d}}` + want = fmt.Sprintf(want, id) + _, pendingTxsGot, err := conn1.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(pendingTxsGot)) +} + +func TestSubscribePendingTxsFilter(t *testing.T) { + t.Parallel() + + log := utils.NewNopZapLogger() + chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) + syncer := newFakeSyncer() + handler := rpc.New(chain, syncer, nil, "", utils.NewNopZapLogger()) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + go func() { + require.NoError(t, handler.Run(ctx)) + }() + time.Sleep(50 * time.Millisecond) + + server := jsonrpc.NewServer(1, log) + require.NoError(t, server.RegisterMethods(jsonrpc.Method{ + Name: "starknet_subscribePendingTransactions", + Params: []jsonrpc.Parameter{{Name: "transaction_details", Optional: true}, {Name: "sender_address", Optional: true}}, + Handler: handler.SubscribePendingTxs, + })) + ws := jsonrpc.NewWebsocket(server, log) + httpSrv := httptest.NewServer(ws) + + conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) + require.NoError(t, err) + + subscribeMsg := []byte(`{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"sender_address":["0xb", "0x16"]}}`) + + id := uint64(1) + handler.WithIDGen(func() uint64 { return id }) + require.NoError(t, conn1.Write(ctx, websocket.MessageText, subscribeMsg)) + + want := fmt.Sprintf(subscribeResponse, id) + _, got, err := conn1.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(got)) + + hash1 := new(felt.Felt).SetUint64(1) + addr1 := new(felt.Felt).SetUint64(11) + + hash2 := new(felt.Felt).SetUint64(2) + addr2 := new(felt.Felt).SetUint64(22) + + hash3 := new(felt.Felt).SetUint64(3) + hash4 := new(felt.Felt).SetUint64(4) + hash5 := new(felt.Felt).SetUint64(5) + + hash6 := new(felt.Felt).SetUint64(6) + addr6 := new(felt.Felt).SetUint64(66) + + hash7 := new(felt.Felt).SetUint64(7) + addr7 := new(felt.Felt).SetUint64(77) + + syncer.pendingTxs.Send([]core.Transaction{ + &core.InvokeTransaction{TransactionHash: hash1, SenderAddress: addr1}, + &core.DeclareTransaction{TransactionHash: hash2, SenderAddress: addr2}, + &core.DeployTransaction{TransactionHash: hash3}, + &core.DeployAccountTransaction{DeployTransaction: core.DeployTransaction{TransactionHash: hash4}}, + &core.L1HandlerTransaction{TransactionHash: hash5}, + &core.InvokeTransaction{TransactionHash: hash6, SenderAddress: addr6}, + &core.DeclareTransaction{TransactionHash: hash7, SenderAddress: addr7}, + }) + + want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":["0x1","0x2"],"subscription_id":%d}}` + want = fmt.Sprintf(want, id) + _, pendingTxsGot, err := conn1.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(pendingTxsGot)) +} + +func TestSubscribePendingTxsFullDetails(t *testing.T) { + t.Parallel() + + log := utils.NewNopZapLogger() + chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) + syncer := newFakeSyncer() + handler := rpc.New(chain, syncer, nil, "", log) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + go func() { + require.NoError(t, handler.Run(ctx)) + }() + time.Sleep(50 * time.Millisecond) + + server := jsonrpc.NewServer(1, log) + require.NoError(t, server.RegisterMethods(jsonrpc.Method{ + Name: "starknet_subscribePendingTransactions", + Params: []jsonrpc.Parameter{{Name: "transaction_details", Optional: true}, {Name: "sender_address", Optional: true}}, + Handler: handler.SubscribePendingTxs, + })) + ws := jsonrpc.NewWebsocket(server, log) + httpSrv := httptest.NewServer(ws) + + conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) + require.NoError(t, err) + + subscribeMsg := []byte(`{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"transaction_details": true}}`) + + id := uint64(1) + handler.WithIDGen(func() uint64 { return id }) + require.NoError(t, conn1.Write(ctx, websocket.MessageText, subscribeMsg)) + + want := fmt.Sprintf(subscribeResponse, id) + _, got, err := conn1.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(got)) + + syncer.pendingTxs.Send([]core.Transaction{ + &core.InvokeTransaction{ + TransactionHash: new(felt.Felt).SetUint64(1), + CallData: []*felt.Felt{new(felt.Felt).SetUint64(2)}, + TransactionSignature: []*felt.Felt{new(felt.Felt).SetUint64(3)}, + MaxFee: new(felt.Felt).SetUint64(4), + ContractAddress: new(felt.Felt).SetUint64(5), + Version: new(core.TransactionVersion).SetUint64(3), + EntryPointSelector: new(felt.Felt).SetUint64(6), + Nonce: new(felt.Felt).SetUint64(7), + SenderAddress: new(felt.Felt).SetUint64(8), + ResourceBounds: map[core.Resource]core.ResourceBounds{}, + Tip: 9, + PaymasterData: []*felt.Felt{new(felt.Felt).SetUint64(10)}, + AccountDeploymentData: []*felt.Felt{new(felt.Felt).SetUint64(11)}, + }, + }) + + want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":[{"transaction_hash":"0x1","type":"INVOKE","version":"0x3","nonce":"0x7","max_fee":"0x4","contract_address":"0x5","sender_address":"0x8","signature":["0x3"],"calldata":["0x2"],"entry_point_selector":"0x6","resource_bounds":{},"tip":"0x9","paymaster_data":["0xa"],"account_deployment_data":["0xb"],"nonce_data_availability_mode":"L1","fee_data_availability_mode":"L1"}],"subscription_id":%d}}` + want = fmt.Sprintf(want, id) + _, pendingTxsGot, err := conn1.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(pendingTxsGot)) +} + +func testHeader(t *testing.T) *core.Header { + t.Helper() + + header := &core.Header{ + Hash: utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), + ParentHash: utils.HexToFelt(t, "0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb"), + Number: 2, + GlobalStateRoot: utils.HexToFelt(t, "0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9"), + Timestamp: 1637084470, + SequencerAddress: utils.HexToFelt(t, "0x0"), + L1DataGasPrice: &core.GasPrice{ + PriceInFri: utils.HexToFelt(t, "0x0"), + PriceInWei: utils.HexToFelt(t, "0x0"), + }, + GasPrice: utils.HexToFelt(t, "0x0"), + GasPriceSTRK: utils.HexToFelt(t, "0x0"), + L1DAMode: core.Calldata, + ProtocolVersion: "", + } + return header } diff --git a/rpc/handlers.go b/rpc/handlers.go index d3ef4d8144..b4281188b8 100644 --- a/rpc/handlers.go +++ b/rpc/handlers.go @@ -69,7 +69,8 @@ var ( ErrTooManyBlocksBack = &jsonrpc.Error{Code: 68, Message: fmt.Sprintf("Cannot go back more than %v blocks", maxBlocksBack)} ErrCallOnPending = &jsonrpc.Error{Code: 69, Message: "This method does not support being called on the pending block"} - ErrTooManyBlocksBack = &jsonrpc.Error{Code: 68, Message: "Cannot go back more than 1024 blocks"} + ErrTooManyAddressesInFilter = &jsonrpc.Error{Code: 67, Message: "Too many addresses in filter sender_address filter"} + ErrTooManyBlocksBack = &jsonrpc.Error{Code: 68, Message: "Cannot go back more than 1024 blocks"} // These errors can be only be returned by Juno-specific methods. ErrSubscriptionNotFound = &jsonrpc.Error{Code: 100, Message: "Subscription not found"} @@ -95,9 +96,10 @@ type Handler struct { vm vm.VM log utils.Logger - version string - newHeads *feed.Feed[*core.Header] - reorgs *feed.Feed[*sync.ReorgData] + version string + newHeads *feed.Feed[*core.Header] + reorgs *feed.Feed[*sync.ReorgData] + pendingTxs *feed.Feed[[]core.Transaction] idgen func() uint64 mu stdsync.Mutex // protects subscriptions. @@ -139,6 +141,7 @@ func New(bcReader blockchain.Reader, syncReader sync.Reader, virtualMachine vm.V version: version, newHeads: feed.New[*core.Header](), reorgs: feed.New[*sync.ReorgData](), + pendingTxs: feed.New[[]core.Transaction](), subscriptions: make(map[uint64]*subscription), blockTraceCache: lru.NewCache[traceCacheKey, []TracedBlockTransaction](traceCacheSize), @@ -181,10 +184,13 @@ func (h *Handler) WithGateway(gatewayClient Gateway) *Handler { func (h *Handler) Run(ctx context.Context) error { newHeadsSub := h.syncReader.SubscribeNewHeads().Subscription reorgsSub := h.syncReader.SubscribeReorg().Subscription + pendingTxsSub := h.syncReader.SubscribePendingTxs().Subscription defer newHeadsSub.Unsubscribe() defer reorgsSub.Unsubscribe() + defer pendingTxsSub.Unsubscribe() feed.Tee(newHeadsSub, h.newHeads) feed.Tee(reorgsSub, h.reorgs) + feed.Tee(pendingTxsSub, h.pendingTxs) <-ctx.Done() for _, sub := range h.subscriptions { @@ -515,11 +521,17 @@ func (h *Handler) MethodsV0_7() ([]jsonrpc.Method, string) { //nolint: funlen Name: "starknet_specVersion", Handler: h.SpecVersionV0_7, }, + { Name: "starknet_subscribeNewHeads", Params: []jsonrpc.Parameter{{Name: "block", Optional: true}}, Handler: h.SubscribeNewHeads, }, + { + Name: "starknet_subscribePendingTransactions", + Params: []jsonrpc.Parameter{{Name: "transaction_details", Optional: true}, {Name: "sender_address", Optional: true}}, + Handler: h.SubscribePendingTxs, + }, { Name: "juno_unsubscribe", Params: []jsonrpc.Parameter{{Name: "id"}}, diff --git a/sync/sync.go b/sync/sync.go index 289fa658d9..c45e1f55bb 100644 --- a/sync/sync.go +++ b/sync/sync.go @@ -42,6 +42,10 @@ type ReorgSubscription struct { *feed.Subscription[*ReorgData] } +type PendingTxSubscription struct { + *feed.Subscription[[]core.Transaction] +} + // Todo: Since this is also going to be implemented by p2p package we should move this interface to node package // //go:generate mockgen -destination=../mocks/mock_synchronizer.go -package=mocks -mock_names Reader=MockSyncReader github.com/NethermindEth/juno/sync Reader @@ -50,6 +54,7 @@ type Reader interface { HighestBlockHeader() *core.Header SubscribeNewHeads() HeaderSubscription SubscribeReorg() ReorgSubscription + SubscribePendingTxs() PendingTxSubscription } // This is temporary and will be removed once the p2p synchronizer implements this interface. @@ -71,6 +76,10 @@ func (n *NoopSynchronizer) SubscribeReorg() ReorgSubscription { return ReorgSubscription{feed.New[*ReorgData]().Subscribe()} } +func (n *NoopSynchronizer) SubscribePendingTxs() PendingTxSubscription { + return PendingTxSubscription{feed.New[[]core.Transaction]().Subscribe()} +} + // ReorgData represents data about reorganised blocks, starting and ending block number and hash type ReorgData struct { // StartBlockHash is the hash of the first known block of the orphaned chain @@ -93,6 +102,7 @@ type Synchronizer struct { highestBlockHeader atomic.Pointer[core.Header] newHeads *feed.Feed[*core.Header] reorgFeed *feed.Feed[*ReorgData] + pendingTxsFeed *feed.Feed[[]core.Transaction] log utils.SimpleLogger listener EventListener @@ -115,6 +125,7 @@ func New(bc *blockchain.Blockchain, starkNetData starknetdata.StarknetData, log log: log, newHeads: feed.New[*core.Header](), reorgFeed: feed.New[*ReorgData](), + pendingTxsFeed: feed.New[[]core.Transaction](), pendingPollInterval: pendingPollInterval, listener: &SelectiveListener{}, readOnlyBlockchain: readOnlyBlockchain, @@ -521,6 +532,9 @@ func (s *Synchronizer) fetchAndStorePending(ctx context.Context) error { return err } + // send the pending transactions to the feed + s.pendingTxsFeed.Send(pendingBlock.Transactions) + s.log.Debugw("Found pending block", "txns", pendingBlock.TransactionCount) return s.StorePending(&Pending{ Block: pendingBlock, @@ -551,3 +565,9 @@ func (s *Synchronizer) SubscribeReorg() ReorgSubscription { Subscription: s.reorgFeed.Subscribe(), } } + +func (s *Synchronizer) SubscribePendingTxs() PendingTxSubscription { + return PendingTxSubscription{ + Subscription: s.pendingTxsFeed.Subscribe(), + } +} diff --git a/sync/sync_test.go b/sync/sync_test.go index ffe8474baf..8b60963c9d 100644 --- a/sync/sync_test.go +++ b/sync/sync_test.go @@ -21,6 +21,8 @@ import ( "go.uber.org/mock/gomock" ) +var emptyCommitments = core.BlockCommitments{} + const timeout = time.Second func TestSyncBlocks(t *testing.T) { @@ -209,102 +211,27 @@ func TestSubscribeNewHeads(t *testing.T) { sub.Unsubscribe() } -func TestPendingSync(t *testing.T) { +func TestSubscribePendingTxs(t *testing.T) { t.Parallel() client := feeder.NewTestClient(t, &utils.Mainnet) gw := adaptfeeder.New(client) - var synchronizer *sync.Synchronizer testDB := pebble.NewMemTest(t) log := utils.NewNopZapLogger() - bc := blockchain.New(testDB, &utils.Mainnet, synchronizer.PendingBlock) - synchronizer = sync.New(bc, gw, log, time.Millisecond*100, false, testDB) + bc := blockchain.New(testDB, &utils.Mainnet) + synchronizer := sync.New(bc, gw, log, time.Millisecond*100, false) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + sub := synchronizer.SubscribePendingTxs() + require.NoError(t, synchronizer.Run(ctx)) cancel() - head, err := bc.HeadsHeader() - require.NoError(t, err) - pending, err := synchronizer.Pending() - require.NoError(t, err) - assert.Equal(t, head.Hash, pending.Block.ParentHash) -} - -func TestPending(t *testing.T) { - client := feeder.NewTestClient(t, &utils.Mainnet) - gw := adaptfeeder.New(client) - - var synchronizer *sync.Synchronizer - testDB := pebble.NewMemTest(t) - chain := blockchain.New(testDB, &utils.Mainnet, synchronizer.PendingBlock) - synchronizer = sync.New(chain, gw, utils.NewNopZapLogger(), 0, false, testDB) - - b, err := gw.BlockByNumber(context.Background(), 0) - require.NoError(t, err) - su, err := gw.StateUpdate(context.Background(), 0) + pending, err := bc.Pending() require.NoError(t, err) - - t.Run("pending state shouldnt exist if no pending block", func(t *testing.T) { - _, _, err = synchronizer.PendingState() - require.Error(t, err) - }) - - t.Run("cannot store unsupported pending block version", func(t *testing.T) { - pending := &sync.Pending{Block: &core.Block{Header: &core.Header{ProtocolVersion: "1.9.0"}}} - require.Error(t, synchronizer.StorePending(pending)) - }) - - t.Run("store genesis as pending", func(t *testing.T) { - pendingGenesis := &sync.Pending{ - Block: b, - StateUpdate: su, - } - require.NoError(t, synchronizer.StorePending(pendingGenesis)) - - gotPending, pErr := synchronizer.Pending() - require.NoError(t, pErr) - assert.Equal(t, pendingGenesis, gotPending) - }) - - require.NoError(t, chain.Store(b, &core.BlockCommitments{}, su, nil)) - - t.Run("storing a pending too far into the future should fail", func(t *testing.T) { - b, err = gw.BlockByNumber(context.Background(), 2) - require.NoError(t, err) - su, err = gw.StateUpdate(context.Background(), 2) - require.NoError(t, err) - - notExpectedPending := sync.Pending{ - Block: b, - StateUpdate: su, - } - require.ErrorIs(t, synchronizer.StorePending(¬ExpectedPending), blockchain.ErrParentDoesNotMatchHead) - }) - - t.Run("store expected pending block", func(t *testing.T) { - b, err = gw.BlockByNumber(context.Background(), 1) - require.NoError(t, err) - su, err = gw.StateUpdate(context.Background(), 1) - require.NoError(t, err) - - expectedPending := &sync.Pending{ - Block: b, - StateUpdate: su, - } - require.NoError(t, synchronizer.StorePending(expectedPending)) - - gotPending, pErr := synchronizer.Pending() - require.NoError(t, pErr) - assert.Equal(t, expectedPending, gotPending) - }) - - t.Run("get pending state", func(t *testing.T) { - _, pendingStateCloser, pErr := synchronizer.PendingState() - t.Cleanup(func() { - require.NoError(t, pendingStateCloser()) - }) - require.NoError(t, pErr) - }) + pendingTxs, ok := <-sub.Recv() + require.True(t, ok) + require.Equal(t, pending.Block.Transactions, pendingTxs) + sub.Unsubscribe() } From 86946f3991c237769673ab743b28e2846870aa08 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 22 Oct 2024 00:50:20 +0800 Subject: [PATCH 08/26] tidy up tests code --- rpc/events_test.go | 198 ++++++++++++++++++--------------------------- 1 file changed, 78 insertions(+), 120 deletions(-) diff --git a/rpc/events_test.go b/rpc/events_test.go index 73c8f45dbd..75c32e93c2 100644 --- a/rpc/events_test.go +++ b/rpc/events_test.go @@ -255,31 +255,49 @@ func (fs *fakeSyncer) HighestBlockHeader() *core.Header { return nil } -func TestSubscribeNewHeads(t *testing.T) { - t.Parallel() +func setupSubscriptionTest(t *testing.T, ctx context.Context) (*rpc.Handler, *fakeSyncer, *jsonrpc.Server) { + t.Helper() log := utils.NewNopZapLogger() chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) syncer := newFakeSyncer() - handler := rpc.New(chain, syncer, nil, "", utils.NewNopZapLogger()) - - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) + handler := rpc.New(chain, syncer, nil, "", log) go func() { require.NoError(t, handler.Run(ctx)) }() - // Technically, there's a race between goroutine above and the SubscribeNewHeads call down below. - // Sleep for a moment just in case. time.Sleep(50 * time.Millisecond) server := jsonrpc.NewServer(1, log) + + return handler, syncer, server +} + +func sendAndReceiveMessage(t *testing.T, ctx context.Context, conn *websocket.Conn, message string) string { + t.Helper() + + require.NoError(t, conn.Write(ctx, websocket.MessageText, []byte(message))) + + _, response, err := conn.Read(ctx) + require.NoError(t, err) + return string(response) +} + +func TestSubscribeNewHeads(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + handler, syncer, server := setupSubscriptionTest(t, ctx) + require.NoError(t, server.RegisterMethods(jsonrpc.Method{ Name: "starknet_subscribeNewHeads", Params: []jsonrpc.Parameter{{Name: "block", Optional: true}}, Handler: handler.SubscribeNewHeads, })) - ws := jsonrpc.NewWebsocket(server, log) + + ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) httpSrv := httptest.NewServer(ws) conn, _, err := websocket.Dial(ctx, httpSrv.URL, nil) @@ -288,13 +306,10 @@ func TestSubscribeNewHeads(t *testing.T) { id := uint64(1) handler.WithIDGen(func() uint64 { return id }) - subscribeMsg := []byte(`{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads"}`) - require.NoError(t, conn.Write(ctx, websocket.MessageText, subscribeMsg)) - + subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads"}` + got := sendAndReceiveMessage(t, ctx, conn, subscribeMsg) want := fmt.Sprintf(subscribeResponse, id) - _, got, err := conn.Read(ctx) - require.NoError(t, err) - require.Equal(t, want, string(got)) + require.Equal(t, want, got) // Simulate a new block syncer.newHeads.Send(testHeader(t)) @@ -309,22 +324,11 @@ func TestSubscribeNewHeads(t *testing.T) { func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { t.Parallel() - log := utils.NewNopZapLogger() - chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) - syncer := newFakeSyncer() - handler := rpc.New(chain, syncer, nil, "", log) - ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - go func() { - require.NoError(t, handler.Run(ctx)) - }() - // Technically, there's a race between goroutine above and the SubscribeNewHeads call down below. - // Sleep for a moment just in case. - time.Sleep(50 * time.Millisecond) + handler, syncer, server := setupSubscriptionTest(t, ctx) - server := jsonrpc.NewServer(1, log) require.NoError(t, server.RegisterMethods(jsonrpc.Method{ Name: "starknet_subscribeNewHeads", Params: []jsonrpc.Parameter{{Name: "block", Optional: true}}, @@ -334,44 +338,45 @@ func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { Params: []jsonrpc.Parameter{{Name: "id"}}, Handler: handler.Unsubscribe, })) - ws := jsonrpc.NewWebsocket(server, log) + + ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) httpSrv := httptest.NewServer(ws) + conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) require.NoError(t, err) conn2, _, err := websocket.Dial(ctx, httpSrv.URL, nil) require.NoError(t, err) - subscribeMsg := []byte(`{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads"}`) + subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads"}` firstID := uint64(1) secondID := uint64(2) - handler.WithIDGen(func() uint64 { return firstID }) - require.NoError(t, conn1.Write(ctx, websocket.MessageText, subscribeMsg)) + handler.WithIDGen(func() uint64 { return firstID }) firstWant := fmt.Sprintf(subscribeResponse, firstID) - _, firstGot, err := conn1.Read(ctx) + firstGot := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) require.NoError(t, err) - require.Equal(t, firstWant, string(firstGot)) + require.Equal(t, firstWant, firstGot) handler.WithIDGen(func() uint64 { return secondID }) - require.NoError(t, conn2.Write(ctx, websocket.MessageText, subscribeMsg)) secondWant := fmt.Sprintf(subscribeResponse, secondID) - _, secondGot, err := conn2.Read(ctx) + secondGot := sendAndReceiveMessage(t, ctx, conn2, subscribeMsg) require.NoError(t, err) - require.Equal(t, secondWant, string(secondGot)) + require.Equal(t, secondWant, secondGot) // Simulate a new block syncer.newHeads.Send(testHeader(t)) // Receive a block header. - firstWant = fmt.Sprintf(newHeadsResponse, firstID) - _, firstGot, err = conn1.Read(ctx) + firstHeaderWant := fmt.Sprintf(newHeadsResponse, firstID) + _, firstHeaderGot, err := conn1.Read(ctx) require.NoError(t, err) - require.Equal(t, firstWant, string(firstGot)) - secondWant = fmt.Sprintf(newHeadsResponse, secondID) - _, secondGot, err = conn2.Read(ctx) + require.Equal(t, firstHeaderWant, string(firstHeaderGot)) + + secondHeaderWant := fmt.Sprintf(newHeadsResponse, secondID) + _, secondHeaderGot, err := conn2.Read(ctx) require.NoError(t, err) - require.Equal(t, secondWant, string(secondGot)) + require.Equal(t, secondHeaderWant, string(secondHeaderGot)) // Unsubscribe unsubMsg := `{"jsonrpc":"2.0","id":1,"method":"juno_unsubscribe","params":[%d]}` @@ -380,6 +385,8 @@ func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { } func TestSubscribeNewHeadsHistorical(t *testing.T) { + t.Parallel() + log := utils.NewNopZapLogger() client := feeder.NewTestClient(t, &utils.Mainnet) gw := adaptfeeder.New(client) @@ -404,16 +411,16 @@ func TestSubscribeNewHeadsHistorical(t *testing.T) { go func() { require.NoError(t, handler.Run(ctx)) }() - // Technically, there's a race between goroutine above and the SubscribeNewHeads call down below. - // Sleep for a moment just in case. time.Sleep(50 * time.Millisecond) server := jsonrpc.NewServer(1, log) + require.NoError(t, server.RegisterMethods(jsonrpc.Method{ Name: "starknet_subscribeNewHeads", Params: []jsonrpc.Parameter{{Name: "block", Optional: true}}, Handler: handler.SubscribeNewHeads, })) + ws := jsonrpc.NewWebsocket(server, log) httpSrv := httptest.NewServer(ws) @@ -423,13 +430,11 @@ func TestSubscribeNewHeadsHistorical(t *testing.T) { id := uint64(1) handler.WithIDGen(func() uint64 { return id }) - subscribeMsg := []byte(`{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads", "params":{"block":{"block_number":0}}}`) - require.NoError(t, conn.Write(ctx, websocket.MessageText, subscribeMsg)) - + subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads", "params":{"block":{"block_number":0}}}` + got := sendAndReceiveMessage(t, ctx, conn, subscribeMsg) want := fmt.Sprintf(subscribeResponse, id) - _, got, err := conn.Read(ctx) require.NoError(t, err) - require.Equal(t, want, string(got)) + require.Equal(t, want, got) // Check block 0 content want = `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x47c3637b57c2b079b93c61539950c17e868a28f46cdef28f88521067f21e943","parent_hash":"0x0","block_number":0,"new_root":"0x21870ba80540e7831fb21c591ee93481f5ae1bb71ff85a86ddd465be4eddee6","timestamp":1637069048,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` @@ -451,26 +456,18 @@ func TestSubscribeNewHeadsHistorical(t *testing.T) { func TestSubscriptionReorg(t *testing.T) { t.Parallel() - log := utils.NewNopZapLogger() - chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) - syncer := newFakeSyncer() - handler := rpc.New(chain, syncer, nil, "", log) - ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - go func() { - require.NoError(t, handler.Run(ctx)) - }() - time.Sleep(50 * time.Millisecond) + handler, syncer, server := setupSubscriptionTest(t, ctx) - server := jsonrpc.NewServer(1, log) require.NoError(t, server.RegisterMethods(jsonrpc.Method{ Name: "starknet_subscribeNewHeads", Params: []jsonrpc.Parameter{{Name: "block", Optional: true}}, Handler: handler.SubscribeNewHeads, })) - ws := jsonrpc.NewWebsocket(server, log) + + ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) httpSrv := httptest.NewServer(ws) conn, _, err := websocket.Dial(ctx, httpSrv.URL, nil) @@ -479,13 +476,10 @@ func TestSubscriptionReorg(t *testing.T) { id := uint64(1) handler.WithIDGen(func() uint64 { return id }) - subscribeMsg := []byte(`{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads"}`) - require.NoError(t, conn.Write(ctx, websocket.MessageText, subscribeMsg)) - + subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads"}` + got := sendAndReceiveMessage(t, ctx, conn, subscribeMsg) want := fmt.Sprintf(subscribeResponse, id) - _, got, err := conn.Read(ctx) - require.NoError(t, err) - require.Equal(t, want, string(got)) + require.Equal(t, want, got) // Simulate a reorg syncer.reorgs.Send(&sync.ReorgData{ @@ -506,41 +500,29 @@ func TestSubscriptionReorg(t *testing.T) { func TestSubscribePendingTxs(t *testing.T) { t.Parallel() - log := utils.NewNopZapLogger() - chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) - syncer := newFakeSyncer() - handler := rpc.New(chain, syncer, nil, "", utils.NewNopZapLogger()) - ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - go func() { - require.NoError(t, handler.Run(ctx)) - }() - time.Sleep(50 * time.Millisecond) + handler, syncer, server := setupSubscriptionTest(t, ctx) - server := jsonrpc.NewServer(1, log) require.NoError(t, server.RegisterMethods(jsonrpc.Method{ Name: "starknet_subscribePendingTransactions", Params: []jsonrpc.Parameter{{Name: "transaction_details", Optional: true}, {Name: "sender_address", Optional: true}}, Handler: handler.SubscribePendingTxs, })) - ws := jsonrpc.NewWebsocket(server, log) + + ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) httpSrv := httptest.NewServer(ws) conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) require.NoError(t, err) - subscribeMsg := []byte(`{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions"}`) - + subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions"}` id := uint64(1) handler.WithIDGen(func() uint64 { return id }) - require.NoError(t, conn1.Write(ctx, websocket.MessageText, subscribeMsg)) - + got := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) want := fmt.Sprintf(subscribeResponse, id) - _, got, err := conn1.Read(ctx) - require.NoError(t, err) - require.Equal(t, want, string(got)) + require.Equal(t, want, got) hash1 := new(felt.Felt).SetUint64(1) addr1 := new(felt.Felt).SetUint64(11) @@ -570,41 +552,29 @@ func TestSubscribePendingTxs(t *testing.T) { func TestSubscribePendingTxsFilter(t *testing.T) { t.Parallel() - log := utils.NewNopZapLogger() - chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) - syncer := newFakeSyncer() - handler := rpc.New(chain, syncer, nil, "", utils.NewNopZapLogger()) - ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - go func() { - require.NoError(t, handler.Run(ctx)) - }() - time.Sleep(50 * time.Millisecond) + handler, syncer, server := setupSubscriptionTest(t, ctx) - server := jsonrpc.NewServer(1, log) require.NoError(t, server.RegisterMethods(jsonrpc.Method{ Name: "starknet_subscribePendingTransactions", Params: []jsonrpc.Parameter{{Name: "transaction_details", Optional: true}, {Name: "sender_address", Optional: true}}, Handler: handler.SubscribePendingTxs, })) - ws := jsonrpc.NewWebsocket(server, log) + + ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) httpSrv := httptest.NewServer(ws) conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) require.NoError(t, err) - subscribeMsg := []byte(`{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"sender_address":["0xb", "0x16"]}}`) - + subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"sender_address":["0xb", "0x16"]}}` id := uint64(1) handler.WithIDGen(func() uint64 { return id }) - require.NoError(t, conn1.Write(ctx, websocket.MessageText, subscribeMsg)) - + got := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) want := fmt.Sprintf(subscribeResponse, id) - _, got, err := conn1.Read(ctx) - require.NoError(t, err) - require.Equal(t, want, string(got)) + require.Equal(t, want, got) hash1 := new(felt.Felt).SetUint64(1) addr1 := new(felt.Felt).SetUint64(11) @@ -642,41 +612,29 @@ func TestSubscribePendingTxsFilter(t *testing.T) { func TestSubscribePendingTxsFullDetails(t *testing.T) { t.Parallel() - log := utils.NewNopZapLogger() - chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) - syncer := newFakeSyncer() - handler := rpc.New(chain, syncer, nil, "", log) - ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - go func() { - require.NoError(t, handler.Run(ctx)) - }() - time.Sleep(50 * time.Millisecond) + handler, syncer, server := setupSubscriptionTest(t, ctx) - server := jsonrpc.NewServer(1, log) require.NoError(t, server.RegisterMethods(jsonrpc.Method{ Name: "starknet_subscribePendingTransactions", Params: []jsonrpc.Parameter{{Name: "transaction_details", Optional: true}, {Name: "sender_address", Optional: true}}, Handler: handler.SubscribePendingTxs, })) - ws := jsonrpc.NewWebsocket(server, log) + + ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) httpSrv := httptest.NewServer(ws) conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) require.NoError(t, err) - subscribeMsg := []byte(`{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"transaction_details": true}}`) - + subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"transaction_details": true}}` id := uint64(1) handler.WithIDGen(func() uint64 { return id }) - require.NoError(t, conn1.Write(ctx, websocket.MessageText, subscribeMsg)) - + got := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) want := fmt.Sprintf(subscribeResponse, id) - _, got, err := conn1.Read(ctx) - require.NoError(t, err) - require.Equal(t, want, string(got)) + require.Equal(t, want, got) syncer.pendingTxs.Send([]core.Transaction{ &core.InvokeTransaction{ From cb0d063c7b280a52cf7dcefecc78f3c54a70b540 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 22 Oct 2024 00:56:57 +0800 Subject: [PATCH 09/26] clean up more --- rpc/events_test.go | 308 ++++++++++++++++++++------------------------- sync/sync_test.go | 2 - 2 files changed, 138 insertions(+), 172 deletions(-) diff --git a/rpc/events_test.go b/rpc/events_test.go index 75c32e93c2..b0198268a8 100644 --- a/rpc/events_test.go +++ b/rpc/events_test.go @@ -26,6 +26,7 @@ import ( var emptyCommitments = core.BlockCommitments{} const ( + subscribeNewHeads = `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads"}` newHeadsResponse = `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` subscribeResponse = `{"jsonrpc":"2.0","result":{"subscription_id":%d},"id":1}` ) @@ -255,34 +256,6 @@ func (fs *fakeSyncer) HighestBlockHeader() *core.Header { return nil } -func setupSubscriptionTest(t *testing.T, ctx context.Context) (*rpc.Handler, *fakeSyncer, *jsonrpc.Server) { - t.Helper() - - log := utils.NewNopZapLogger() - chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) - syncer := newFakeSyncer() - handler := rpc.New(chain, syncer, nil, "", log) - - go func() { - require.NoError(t, handler.Run(ctx)) - }() - time.Sleep(50 * time.Millisecond) - - server := jsonrpc.NewServer(1, log) - - return handler, syncer, server -} - -func sendAndReceiveMessage(t *testing.T, ctx context.Context, conn *websocket.Conn, message string) string { - t.Helper() - - require.NoError(t, conn.Write(ctx, websocket.MessageText, []byte(message))) - - _, response, err := conn.Read(ctx) - require.NoError(t, err) - return string(response) -} - func TestSubscribeNewHeads(t *testing.T) { t.Parallel() @@ -306,8 +279,7 @@ func TestSubscribeNewHeads(t *testing.T) { id := uint64(1) handler.WithIDGen(func() uint64 { return id }) - subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads"}` - got := sendAndReceiveMessage(t, ctx, conn, subscribeMsg) + got := sendAndReceiveMessage(t, ctx, conn, subscribeNewHeads) want := fmt.Sprintf(subscribeResponse, id) require.Equal(t, want, got) @@ -347,20 +319,18 @@ func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { conn2, _, err := websocket.Dial(ctx, httpSrv.URL, nil) require.NoError(t, err) - subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads"}` - firstID := uint64(1) secondID := uint64(2) handler.WithIDGen(func() uint64 { return firstID }) firstWant := fmt.Sprintf(subscribeResponse, firstID) - firstGot := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) + firstGot := sendAndReceiveMessage(t, ctx, conn1, subscribeNewHeads) require.NoError(t, err) require.Equal(t, firstWant, firstGot) handler.WithIDGen(func() uint64 { return secondID }) secondWant := fmt.Sprintf(subscribeResponse, secondID) - secondGot := sendAndReceiveMessage(t, ctx, conn2, subscribeMsg) + secondGot := sendAndReceiveMessage(t, ctx, conn2, subscribeNewHeads) require.NoError(t, err) require.Equal(t, secondWant, secondGot) @@ -476,8 +446,7 @@ func TestSubscriptionReorg(t *testing.T) { id := uint64(1) handler.WithIDGen(func() uint64 { return id }) - subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads"}` - got := sendAndReceiveMessage(t, ctx, conn, subscribeMsg) + got := sendAndReceiveMessage(t, ctx, conn, subscribeNewHeads) want := fmt.Sprintf(subscribeResponse, id) require.Equal(t, want, got) @@ -514,151 +483,122 @@ func TestSubscribePendingTxs(t *testing.T) { ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) httpSrv := httptest.NewServer(ws) - conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) - require.NoError(t, err) - - subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions"}` - id := uint64(1) - handler.WithIDGen(func() uint64 { return id }) - got := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) - want := fmt.Sprintf(subscribeResponse, id) - require.Equal(t, want, got) - - hash1 := new(felt.Felt).SetUint64(1) - addr1 := new(felt.Felt).SetUint64(11) - - hash2 := new(felt.Felt).SetUint64(2) - addr2 := new(felt.Felt).SetUint64(22) + t.Run("Basic subscription", func(t *testing.T) { + conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) + require.NoError(t, err) - hash3 := new(felt.Felt).SetUint64(3) - hash4 := new(felt.Felt).SetUint64(4) - hash5 := new(felt.Felt).SetUint64(5) + subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions"}` + id := uint64(1) + handler.WithIDGen(func() uint64 { return id }) + got := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) + want := fmt.Sprintf(subscribeResponse, id) + require.Equal(t, want, got) + + hash1 := new(felt.Felt).SetUint64(1) + addr1 := new(felt.Felt).SetUint64(11) + + hash2 := new(felt.Felt).SetUint64(2) + addr2 := new(felt.Felt).SetUint64(22) + + hash3 := new(felt.Felt).SetUint64(3) + hash4 := new(felt.Felt).SetUint64(4) + hash5 := new(felt.Felt).SetUint64(5) + + syncer.pendingTxs.Send([]core.Transaction{ + &core.InvokeTransaction{TransactionHash: hash1, SenderAddress: addr1}, + &core.DeclareTransaction{TransactionHash: hash2, SenderAddress: addr2}, + &core.DeployTransaction{TransactionHash: hash3}, + &core.DeployAccountTransaction{DeployTransaction: core.DeployTransaction{TransactionHash: hash4}}, + &core.L1HandlerTransaction{TransactionHash: hash5}, + }) - syncer.pendingTxs.Send([]core.Transaction{ - &core.InvokeTransaction{TransactionHash: hash1, SenderAddress: addr1}, - &core.DeclareTransaction{TransactionHash: hash2, SenderAddress: addr2}, - &core.DeployTransaction{TransactionHash: hash3}, - &core.DeployAccountTransaction{DeployTransaction: core.DeployTransaction{TransactionHash: hash4}}, - &core.L1HandlerTransaction{TransactionHash: hash5}, + want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":["0x1","0x2","0x3","0x4","0x5"],"subscription_id":%d}}` + want = fmt.Sprintf(want, id) + _, pendingTxsGot, err := conn1.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(pendingTxsGot)) }) - want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":["0x1","0x2","0x3","0x4","0x5"],"subscription_id":%d}}` - want = fmt.Sprintf(want, id) - _, pendingTxsGot, err := conn1.Read(ctx) - require.NoError(t, err) - require.Equal(t, want, string(pendingTxsGot)) -} - -func TestSubscribePendingTxsFilter(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - handler, syncer, server := setupSubscriptionTest(t, ctx) - - require.NoError(t, server.RegisterMethods(jsonrpc.Method{ - Name: "starknet_subscribePendingTransactions", - Params: []jsonrpc.Parameter{{Name: "transaction_details", Optional: true}, {Name: "sender_address", Optional: true}}, - Handler: handler.SubscribePendingTxs, - })) - - ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) - httpSrv := httptest.NewServer(ws) - - conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) - require.NoError(t, err) - - subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"sender_address":["0xb", "0x16"]}}` - id := uint64(1) - handler.WithIDGen(func() uint64 { return id }) - got := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) - want := fmt.Sprintf(subscribeResponse, id) - require.Equal(t, want, got) - - hash1 := new(felt.Felt).SetUint64(1) - addr1 := new(felt.Felt).SetUint64(11) - - hash2 := new(felt.Felt).SetUint64(2) - addr2 := new(felt.Felt).SetUint64(22) - - hash3 := new(felt.Felt).SetUint64(3) - hash4 := new(felt.Felt).SetUint64(4) - hash5 := new(felt.Felt).SetUint64(5) - - hash6 := new(felt.Felt).SetUint64(6) - addr6 := new(felt.Felt).SetUint64(66) + t.Run("Filtered subscription", func(t *testing.T) { + conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) + require.NoError(t, err) - hash7 := new(felt.Felt).SetUint64(7) - addr7 := new(felt.Felt).SetUint64(77) + subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"sender_address":["0xb", "0x16"]}}` + id := uint64(1) + handler.WithIDGen(func() uint64 { return id }) + got := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) + want := fmt.Sprintf(subscribeResponse, id) + require.Equal(t, want, got) + + hash1 := new(felt.Felt).SetUint64(1) + addr1 := new(felt.Felt).SetUint64(11) + + hash2 := new(felt.Felt).SetUint64(2) + addr2 := new(felt.Felt).SetUint64(22) + + hash3 := new(felt.Felt).SetUint64(3) + hash4 := new(felt.Felt).SetUint64(4) + hash5 := new(felt.Felt).SetUint64(5) + + hash6 := new(felt.Felt).SetUint64(6) + addr6 := new(felt.Felt).SetUint64(66) + + hash7 := new(felt.Felt).SetUint64(7) + addr7 := new(felt.Felt).SetUint64(77) + + syncer.pendingTxs.Send([]core.Transaction{ + &core.InvokeTransaction{TransactionHash: hash1, SenderAddress: addr1}, + &core.DeclareTransaction{TransactionHash: hash2, SenderAddress: addr2}, + &core.DeployTransaction{TransactionHash: hash3}, + &core.DeployAccountTransaction{DeployTransaction: core.DeployTransaction{TransactionHash: hash4}}, + &core.L1HandlerTransaction{TransactionHash: hash5}, + &core.InvokeTransaction{TransactionHash: hash6, SenderAddress: addr6}, + &core.DeclareTransaction{TransactionHash: hash7, SenderAddress: addr7}, + }) - syncer.pendingTxs.Send([]core.Transaction{ - &core.InvokeTransaction{TransactionHash: hash1, SenderAddress: addr1}, - &core.DeclareTransaction{TransactionHash: hash2, SenderAddress: addr2}, - &core.DeployTransaction{TransactionHash: hash3}, - &core.DeployAccountTransaction{DeployTransaction: core.DeployTransaction{TransactionHash: hash4}}, - &core.L1HandlerTransaction{TransactionHash: hash5}, - &core.InvokeTransaction{TransactionHash: hash6, SenderAddress: addr6}, - &core.DeclareTransaction{TransactionHash: hash7, SenderAddress: addr7}, + want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":["0x1","0x2"],"subscription_id":%d}}` + want = fmt.Sprintf(want, id) + _, pendingTxsGot, err := conn1.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(pendingTxsGot)) }) - want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":["0x1","0x2"],"subscription_id":%d}}` - want = fmt.Sprintf(want, id) - _, pendingTxsGot, err := conn1.Read(ctx) - require.NoError(t, err) - require.Equal(t, want, string(pendingTxsGot)) -} - -func TestSubscribePendingTxsFullDetails(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - handler, syncer, server := setupSubscriptionTest(t, ctx) - - require.NoError(t, server.RegisterMethods(jsonrpc.Method{ - Name: "starknet_subscribePendingTransactions", - Params: []jsonrpc.Parameter{{Name: "transaction_details", Optional: true}, {Name: "sender_address", Optional: true}}, - Handler: handler.SubscribePendingTxs, - })) - - ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) - httpSrv := httptest.NewServer(ws) - - conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) - require.NoError(t, err) + t.Run("Full details subscription", func(t *testing.T) { + t.Parallel() + conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) + require.NoError(t, err) - subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"transaction_details": true}}` - id := uint64(1) - handler.WithIDGen(func() uint64 { return id }) - got := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) - want := fmt.Sprintf(subscribeResponse, id) - require.Equal(t, want, got) + subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"transaction_details": true}}` + id := uint64(1) + handler.WithIDGen(func() uint64 { return id }) + got := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) + want := fmt.Sprintf(subscribeResponse, id) + require.Equal(t, want, got) + + syncer.pendingTxs.Send([]core.Transaction{ + &core.InvokeTransaction{ + TransactionHash: new(felt.Felt).SetUint64(1), + CallData: []*felt.Felt{new(felt.Felt).SetUint64(2)}, + TransactionSignature: []*felt.Felt{new(felt.Felt).SetUint64(3)}, + MaxFee: new(felt.Felt).SetUint64(4), + ContractAddress: new(felt.Felt).SetUint64(5), + Version: new(core.TransactionVersion).SetUint64(3), + EntryPointSelector: new(felt.Felt).SetUint64(6), + Nonce: new(felt.Felt).SetUint64(7), + SenderAddress: new(felt.Felt).SetUint64(8), + ResourceBounds: map[core.Resource]core.ResourceBounds{}, + Tip: 9, + PaymasterData: []*felt.Felt{new(felt.Felt).SetUint64(10)}, + AccountDeploymentData: []*felt.Felt{new(felt.Felt).SetUint64(11)}, + }, + }) - syncer.pendingTxs.Send([]core.Transaction{ - &core.InvokeTransaction{ - TransactionHash: new(felt.Felt).SetUint64(1), - CallData: []*felt.Felt{new(felt.Felt).SetUint64(2)}, - TransactionSignature: []*felt.Felt{new(felt.Felt).SetUint64(3)}, - MaxFee: new(felt.Felt).SetUint64(4), - ContractAddress: new(felt.Felt).SetUint64(5), - Version: new(core.TransactionVersion).SetUint64(3), - EntryPointSelector: new(felt.Felt).SetUint64(6), - Nonce: new(felt.Felt).SetUint64(7), - SenderAddress: new(felt.Felt).SetUint64(8), - ResourceBounds: map[core.Resource]core.ResourceBounds{}, - Tip: 9, - PaymasterData: []*felt.Felt{new(felt.Felt).SetUint64(10)}, - AccountDeploymentData: []*felt.Felt{new(felt.Felt).SetUint64(11)}, - }, + want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":[{"transaction_hash":"0x1","type":"INVOKE","version":"0x3","nonce":"0x7","max_fee":"0x4","contract_address":"0x5","sender_address":"0x8","signature":["0x3"],"calldata":["0x2"],"entry_point_selector":"0x6","resource_bounds":{},"tip":"0x9","paymaster_data":["0xa"],"account_deployment_data":["0xb"],"nonce_data_availability_mode":"L1","fee_data_availability_mode":"L1"}],"subscription_id":%d}}` + want = fmt.Sprintf(want, id) + _, pendingTxsGot, err := conn1.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(pendingTxsGot)) }) - - want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":[{"transaction_hash":"0x1","type":"INVOKE","version":"0x3","nonce":"0x7","max_fee":"0x4","contract_address":"0x5","sender_address":"0x8","signature":["0x3"],"calldata":["0x2"],"entry_point_selector":"0x6","resource_bounds":{},"tip":"0x9","paymaster_data":["0xa"],"account_deployment_data":["0xb"],"nonce_data_availability_mode":"L1","fee_data_availability_mode":"L1"}],"subscription_id":%d}}` - want = fmt.Sprintf(want, id) - _, pendingTxsGot, err := conn1.Read(ctx) - require.NoError(t, err) - require.Equal(t, want, string(pendingTxsGot)) } func testHeader(t *testing.T) *core.Header { @@ -682,3 +622,31 @@ func testHeader(t *testing.T) *core.Header { } return header } + +func setupSubscriptionTest(t *testing.T, ctx context.Context) (*rpc.Handler, *fakeSyncer, *jsonrpc.Server) { + t.Helper() + + log := utils.NewNopZapLogger() + chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) + syncer := newFakeSyncer() + handler := rpc.New(chain, syncer, nil, "", log) + + go func() { + require.NoError(t, handler.Run(ctx)) + }() + time.Sleep(50 * time.Millisecond) + + server := jsonrpc.NewServer(1, log) + + return handler, syncer, server +} + +func sendAndReceiveMessage(t *testing.T, ctx context.Context, conn *websocket.Conn, message string) string { + t.Helper() + + require.NoError(t, conn.Write(ctx, websocket.MessageText, []byte(message))) + + _, response, err := conn.Read(ctx) + require.NoError(t, err) + return string(response) +} diff --git a/sync/sync_test.go b/sync/sync_test.go index 8b60963c9d..3839154322 100644 --- a/sync/sync_test.go +++ b/sync/sync_test.go @@ -21,8 +21,6 @@ import ( "go.uber.org/mock/gomock" ) -var emptyCommitments = core.BlockCommitments{} - const timeout = time.Second func TestSyncBlocks(t *testing.T) { From ae1024495493dde19e8a6966c929d0132f9da2a1 Mon Sep 17 00:00:00 2001 From: weiihann Date: Fri, 25 Oct 2024 19:03:32 +0800 Subject: [PATCH 10/26] put IsNil in utils package --- adapters/p2p2core/felt.go | 4 ++-- jsonrpc/server.go | 8 ++------ utils/check.go | 7 +++++++ 3 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 utils/check.go diff --git a/adapters/p2p2core/felt.go b/adapters/p2p2core/felt.go index cd4b3080b2..a823415c42 100644 --- a/adapters/p2p2core/felt.go +++ b/adapters/p2p2core/felt.go @@ -2,10 +2,10 @@ package p2p2core import ( "encoding/binary" - "reflect" "github.com/NethermindEth/juno/core/felt" "github.com/NethermindEth/juno/p2p/starknet/spec" + "github.com/NethermindEth/juno/utils" "github.com/ethereum/go-ethereum/common" ) @@ -28,7 +28,7 @@ func AdaptFelt(f *spec.Felt252) *felt.Felt { func adapt(v interface{ GetElements() []byte }) *felt.Felt { // when passing a nil pointer `v` is boxed to an interface and is not nil // see: https://blog.devtrovert.com/p/go-secret-interface-nil-is-not-nil - if v == nil || reflect.ValueOf(v).IsNil() { + if utils.IsNil(v) { return nil } diff --git a/jsonrpc/server.go b/jsonrpc/server.go index 888c0d687a..863b1594f5 100644 --- a/jsonrpc/server.go +++ b/jsonrpc/server.go @@ -422,12 +422,8 @@ func isBatch(reader *bufio.Reader) bool { return false } -func isNil(i any) bool { - return i == nil || reflect.ValueOf(i).IsNil() -} - func isNilOrEmpty(i any) (bool, error) { - if isNil(i) { + if utils.IsNil(i) { return true, nil } @@ -484,7 +480,7 @@ func (s *Server) handleRequest(ctx context.Context, req *Request) (*response, ht header = (tuple[1].Interface()).(http.Header) } - if errAny := tuple[errorIndex].Interface(); !isNil(errAny) { + if errAny := tuple[errorIndex].Interface(); !utils.IsNil(errAny) { res.Error = errAny.(*Error) if res.Error.Code == InternalError { s.listener.OnRequestFailed(req.Method, res.Error) diff --git a/utils/check.go b/utils/check.go new file mode 100644 index 0000000000..56e5c89185 --- /dev/null +++ b/utils/check.go @@ -0,0 +1,7 @@ +package utils + +import "reflect" + +func IsNil(i any) bool { + return i == nil || reflect.ValueOf(i).IsNil() +} From b2176ff5343eb78f3f6b283e9561065b912e8c80 Mon Sep 17 00:00:00 2001 From: weiihann Date: Fri, 25 Oct 2024 19:56:35 +0800 Subject: [PATCH 11/26] register RPC methods automatically in tests --- rpc/events_test.go | 38 ++++---------------------------------- rpc/handlers.go | 11 +++++------ 2 files changed, 9 insertions(+), 40 deletions(-) diff --git a/rpc/events_test.go b/rpc/events_test.go index b0198268a8..7c9faadefb 100644 --- a/rpc/events_test.go +++ b/rpc/events_test.go @@ -264,12 +264,6 @@ func TestSubscribeNewHeads(t *testing.T) { handler, syncer, server := setupSubscriptionTest(t, ctx) - require.NoError(t, server.RegisterMethods(jsonrpc.Method{ - Name: "starknet_subscribeNewHeads", - Params: []jsonrpc.Parameter{{Name: "block", Optional: true}}, - Handler: handler.SubscribeNewHeads, - })) - ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) httpSrv := httptest.NewServer(ws) @@ -301,16 +295,6 @@ func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { handler, syncer, server := setupSubscriptionTest(t, ctx) - require.NoError(t, server.RegisterMethods(jsonrpc.Method{ - Name: "starknet_subscribeNewHeads", - Params: []jsonrpc.Parameter{{Name: "block", Optional: true}}, - Handler: handler.SubscribeNewHeads, - }, jsonrpc.Method{ - Name: "juno_unsubscribe", - Params: []jsonrpc.Parameter{{Name: "id"}}, - Handler: handler.Unsubscribe, - })) - ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) httpSrv := httptest.NewServer(ws) @@ -384,12 +368,8 @@ func TestSubscribeNewHeadsHistorical(t *testing.T) { time.Sleep(50 * time.Millisecond) server := jsonrpc.NewServer(1, log) - - require.NoError(t, server.RegisterMethods(jsonrpc.Method{ - Name: "starknet_subscribeNewHeads", - Params: []jsonrpc.Parameter{{Name: "block", Optional: true}}, - Handler: handler.SubscribeNewHeads, - })) + methods, _ := handler.Methods() + require.NoError(t, server.RegisterMethods(methods...)) ws := jsonrpc.NewWebsocket(server, log) httpSrv := httptest.NewServer(ws) @@ -431,12 +411,6 @@ func TestSubscriptionReorg(t *testing.T) { handler, syncer, server := setupSubscriptionTest(t, ctx) - require.NoError(t, server.RegisterMethods(jsonrpc.Method{ - Name: "starknet_subscribeNewHeads", - Params: []jsonrpc.Parameter{{Name: "block", Optional: true}}, - Handler: handler.SubscribeNewHeads, - })) - ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) httpSrv := httptest.NewServer(ws) @@ -474,12 +448,6 @@ func TestSubscribePendingTxs(t *testing.T) { handler, syncer, server := setupSubscriptionTest(t, ctx) - require.NoError(t, server.RegisterMethods(jsonrpc.Method{ - Name: "starknet_subscribePendingTransactions", - Params: []jsonrpc.Parameter{{Name: "transaction_details", Optional: true}, {Name: "sender_address", Optional: true}}, - Handler: handler.SubscribePendingTxs, - })) - ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) httpSrv := httptest.NewServer(ws) @@ -637,6 +605,8 @@ func setupSubscriptionTest(t *testing.T, ctx context.Context) (*rpc.Handler, *fa time.Sleep(50 * time.Millisecond) server := jsonrpc.NewServer(1, log) + methods, _ := handler.Methods() + require.NoError(t, server.RegisterMethods(methods...)) return handler, syncer, server } diff --git a/rpc/handlers.go b/rpc/handlers.go index b4281188b8..fc541b17b8 100644 --- a/rpc/handlers.go +++ b/rpc/handlers.go @@ -357,6 +357,11 @@ func (h *Handler) Methods() ([]jsonrpc.Method, string) { //nolint: funlen Params: []jsonrpc.Parameter{{Name: "block", Optional: true}}, Handler: h.SubscribeNewHeads, }, + { + Name: "starknet_subscribePendingTransactions", + Params: []jsonrpc.Parameter{{Name: "transaction_details", Optional: true}, {Name: "sender_address", Optional: true}}, + Handler: h.SubscribePendingTxs, + }, { Name: "juno_unsubscribe", Params: []jsonrpc.Parameter{{Name: "id"}}, @@ -521,17 +526,11 @@ func (h *Handler) MethodsV0_7() ([]jsonrpc.Method, string) { //nolint: funlen Name: "starknet_specVersion", Handler: h.SpecVersionV0_7, }, - { Name: "starknet_subscribeNewHeads", Params: []jsonrpc.Parameter{{Name: "block", Optional: true}}, Handler: h.SubscribeNewHeads, }, - { - Name: "starknet_subscribePendingTransactions", - Params: []jsonrpc.Parameter{{Name: "transaction_details", Optional: true}, {Name: "sender_address", Optional: true}}, - Handler: h.SubscribePendingTxs, - }, { Name: "juno_unsubscribe", Params: []jsonrpc.Parameter{{Name: "id"}}, From 5072bfeabcd69c105bc0102d79cd783ad82676d0 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 29 Oct 2024 17:29:47 +0800 Subject: [PATCH 12/26] modify max addresses filter --- rpc/events.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpc/events.go b/rpc/events.go index 6af3c363d0..980af5eabf 100644 --- a/rpc/events.go +++ b/rpc/events.go @@ -15,7 +15,7 @@ import ( const ( MaxBlocksBack = 1024 - MaxAddressesInFilter = 1000 // TODO(weiihann): not finalised yet + MaxAddressesInFilter = 1024 // An arbitrary number, to be revisited when we have more contexts ) type EventsArg struct { From 4b386e0391f2b78ab433ed0d57f54908a9788d08 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 10:04:11 +0800 Subject: [PATCH 13/26] all tests pass but need to clean up --- rpc/events.go | 7 +- rpc/events_test.go | 415 +-------------------------------- rpc/subscriptions_test.go | 473 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 480 insertions(+), 415 deletions(-) diff --git a/rpc/events.go b/rpc/events.go index 980af5eabf..cff7d27902 100644 --- a/rpc/events.go +++ b/rpc/events.go @@ -247,6 +247,10 @@ func (h *Handler) getStartAndLatestHeaders(blockID *BlockID) (*core.Header, *cor return nil, nil, nil } + if blockID.Pending { + return nil, nil, ErrCallOnPending + } + latestHeader, err := h.bcReader.HeadsHeader() if err != nil { return nil, nil, ErrInternal @@ -257,7 +261,8 @@ func (h *Handler) getStartAndLatestHeaders(blockID *BlockID) (*core.Header, *cor return nil, nil, rpcErr } - if latestHeader.Number > MaxBlocksBack && startHeader.Number < latestHeader.Number-MaxBlocksBack { + // TODO(weiihann): also, reuse this function in other places + if latestHeader.Number >= MaxBlocksBack && startHeader.Number <= latestHeader.Number-MaxBlocksBack { return nil, nil, ErrTooManyBlocksBack } diff --git a/rpc/events_test.go b/rpc/events_test.go index 7c9faadefb..34b96faf91 100644 --- a/rpc/events_test.go +++ b/rpc/events_test.go @@ -2,35 +2,21 @@ package rpc_test import ( "context" - "fmt" - "net/http/httptest" "testing" - "time" "github.com/NethermindEth/juno/blockchain" "github.com/NethermindEth/juno/clients/feeder" "github.com/NethermindEth/juno/core" "github.com/NethermindEth/juno/core/felt" "github.com/NethermindEth/juno/db/pebble" - "github.com/NethermindEth/juno/feed" - "github.com/NethermindEth/juno/jsonrpc" + "github.com/NethermindEth/juno/rpc" adaptfeeder "github.com/NethermindEth/juno/starknetdata/feeder" - "github.com/NethermindEth/juno/sync" "github.com/NethermindEth/juno/utils" - "github.com/coder/websocket" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -var emptyCommitments = core.BlockCommitments{} - -const ( - subscribeNewHeads = `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads"}` - newHeadsResponse = `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` - subscribeResponse = `{"jsonrpc":"2.0","result":{"subscription_id":%d},"id":1}` -) - func TestEvents(t *testing.T) { var pendingB *core.Block pendingBlockFn := func() *core.Block { @@ -221,402 +207,3 @@ func TestEvents(t *testing.T) { assert.Equal(t, utils.HexToFelt(t, "0x785c2ada3f53fbc66078d47715c27718f92e6e48b96372b36e5197de69b82b5"), events.Events[0].TransactionHash) }) } - -type fakeSyncer struct { - newHeads *feed.Feed[*core.Header] - reorgs *feed.Feed[*sync.ReorgData] - pendingTxs *feed.Feed[[]core.Transaction] -} - -func newFakeSyncer() *fakeSyncer { - return &fakeSyncer{ - newHeads: feed.New[*core.Header](), - reorgs: feed.New[*sync.ReorgData](), - pendingTxs: feed.New[[]core.Transaction](), - } -} - -func (fs *fakeSyncer) SubscribeNewHeads() sync.HeaderSubscription { - return sync.HeaderSubscription{Subscription: fs.newHeads.Subscribe()} -} - -func (fs *fakeSyncer) SubscribeReorg() sync.ReorgSubscription { - return sync.ReorgSubscription{Subscription: fs.reorgs.Subscribe()} -} - -func (fs *fakeSyncer) SubscribePendingTxs() sync.PendingTxSubscription { - return sync.PendingTxSubscription{Subscription: fs.pendingTxs.Subscribe()} -} - -func (fs *fakeSyncer) StartingBlockNumber() (uint64, error) { - return 0, nil -} - -func (fs *fakeSyncer) HighestBlockHeader() *core.Header { - return nil -} - -func TestSubscribeNewHeads(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - handler, syncer, server := setupSubscriptionTest(t, ctx) - - ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) - httpSrv := httptest.NewServer(ws) - - conn, _, err := websocket.Dial(ctx, httpSrv.URL, nil) - require.NoError(t, err) - - id := uint64(1) - handler.WithIDGen(func() uint64 { return id }) - - got := sendAndReceiveMessage(t, ctx, conn, subscribeNewHeads) - want := fmt.Sprintf(subscribeResponse, id) - require.Equal(t, want, got) - - // Simulate a new block - syncer.newHeads.Send(testHeader(t)) - - // Receive a block header. - want = fmt.Sprintf(newHeadsResponse, id) - _, headerGot, err := conn.Read(ctx) - require.NoError(t, err) - require.Equal(t, want, string(headerGot)) -} - -func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - handler, syncer, server := setupSubscriptionTest(t, ctx) - - ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) - httpSrv := httptest.NewServer(ws) - - conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) - require.NoError(t, err) - conn2, _, err := websocket.Dial(ctx, httpSrv.URL, nil) - require.NoError(t, err) - - firstID := uint64(1) - secondID := uint64(2) - - handler.WithIDGen(func() uint64 { return firstID }) - firstWant := fmt.Sprintf(subscribeResponse, firstID) - firstGot := sendAndReceiveMessage(t, ctx, conn1, subscribeNewHeads) - require.NoError(t, err) - require.Equal(t, firstWant, firstGot) - - handler.WithIDGen(func() uint64 { return secondID }) - secondWant := fmt.Sprintf(subscribeResponse, secondID) - secondGot := sendAndReceiveMessage(t, ctx, conn2, subscribeNewHeads) - require.NoError(t, err) - require.Equal(t, secondWant, secondGot) - - // Simulate a new block - syncer.newHeads.Send(testHeader(t)) - - // Receive a block header. - firstHeaderWant := fmt.Sprintf(newHeadsResponse, firstID) - _, firstHeaderGot, err := conn1.Read(ctx) - require.NoError(t, err) - require.Equal(t, firstHeaderWant, string(firstHeaderGot)) - - secondHeaderWant := fmt.Sprintf(newHeadsResponse, secondID) - _, secondHeaderGot, err := conn2.Read(ctx) - require.NoError(t, err) - require.Equal(t, secondHeaderWant, string(secondHeaderGot)) - - // Unsubscribe - unsubMsg := `{"jsonrpc":"2.0","id":1,"method":"juno_unsubscribe","params":[%d]}` - require.NoError(t, conn1.Write(ctx, websocket.MessageBinary, []byte(fmt.Sprintf(unsubMsg, firstID)))) - require.NoError(t, conn2.Write(ctx, websocket.MessageBinary, []byte(fmt.Sprintf(unsubMsg, secondID)))) -} - -func TestSubscribeNewHeadsHistorical(t *testing.T) { - t.Parallel() - - log := utils.NewNopZapLogger() - client := feeder.NewTestClient(t, &utils.Mainnet) - gw := adaptfeeder.New(client) - - block0, err := gw.BlockByNumber(context.Background(), 0) - require.NoError(t, err) - - stateUpdate0, err := gw.StateUpdate(context.Background(), 0) - require.NoError(t, err) - - testDB := pebble.NewMemTest(t) - chain := blockchain.New(testDB, &utils.Mainnet) - assert.NoError(t, chain.Store(block0, &emptyCommitments, stateUpdate0, nil)) - - chain = blockchain.New(testDB, &utils.Mainnet) - syncer := newFakeSyncer() - handler := rpc.New(chain, syncer, nil, "", utils.NewNopZapLogger()) - - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - go func() { - require.NoError(t, handler.Run(ctx)) - }() - time.Sleep(50 * time.Millisecond) - - server := jsonrpc.NewServer(1, log) - methods, _ := handler.Methods() - require.NoError(t, server.RegisterMethods(methods...)) - - ws := jsonrpc.NewWebsocket(server, log) - httpSrv := httptest.NewServer(ws) - - conn, _, err := websocket.Dial(ctx, httpSrv.URL, nil) - require.NoError(t, err) - - id := uint64(1) - handler.WithIDGen(func() uint64 { return id }) - - subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads", "params":{"block":{"block_number":0}}}` - got := sendAndReceiveMessage(t, ctx, conn, subscribeMsg) - want := fmt.Sprintf(subscribeResponse, id) - require.NoError(t, err) - require.Equal(t, want, got) - - // Check block 0 content - want = `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x47c3637b57c2b079b93c61539950c17e868a28f46cdef28f88521067f21e943","parent_hash":"0x0","block_number":0,"new_root":"0x21870ba80540e7831fb21c591ee93481f5ae1bb71ff85a86ddd465be4eddee6","timestamp":1637069048,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` - want = fmt.Sprintf(want, id) - _, block0Got, err := conn.Read(ctx) - require.NoError(t, err) - require.Equal(t, want, string(block0Got)) - - // Simulate a new block - syncer.newHeads.Send(testHeader(t)) - - // Check new block content - want = fmt.Sprintf(newHeadsResponse, id) - _, newBlockGot, err := conn.Read(ctx) - require.NoError(t, err) - require.Equal(t, want, string(newBlockGot)) -} - -func TestSubscriptionReorg(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - handler, syncer, server := setupSubscriptionTest(t, ctx) - - ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) - httpSrv := httptest.NewServer(ws) - - conn, _, err := websocket.Dial(ctx, httpSrv.URL, nil) - require.NoError(t, err) - - id := uint64(1) - handler.WithIDGen(func() uint64 { return id }) - - got := sendAndReceiveMessage(t, ctx, conn, subscribeNewHeads) - want := fmt.Sprintf(subscribeResponse, id) - require.Equal(t, want, got) - - // Simulate a reorg - syncer.reorgs.Send(&sync.ReorgData{ - StartBlockHash: utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), - StartBlockNum: 0, - EndBlockHash: utils.HexToFelt(t, "0x34e815552e42c5eb5233b99de2d3d7fd396e575df2719bf98e7ed2794494f86"), - EndBlockNum: 2, - }) - - // Receive reorg event - want = `{"jsonrpc":"2.0","method":"starknet_subscriptionReorg","params":{"result":{"starting_block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","starting_block_number":0,"ending_block_hash":"0x34e815552e42c5eb5233b99de2d3d7fd396e575df2719bf98e7ed2794494f86","ending_block_number":2},"subscription_id":%d}}` - want = fmt.Sprintf(want, id) - _, reorgGot, err := conn.Read(ctx) - require.NoError(t, err) - require.Equal(t, want, string(reorgGot)) -} - -func TestSubscribePendingTxs(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - handler, syncer, server := setupSubscriptionTest(t, ctx) - - ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) - httpSrv := httptest.NewServer(ws) - - t.Run("Basic subscription", func(t *testing.T) { - conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) - require.NoError(t, err) - - subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions"}` - id := uint64(1) - handler.WithIDGen(func() uint64 { return id }) - got := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) - want := fmt.Sprintf(subscribeResponse, id) - require.Equal(t, want, got) - - hash1 := new(felt.Felt).SetUint64(1) - addr1 := new(felt.Felt).SetUint64(11) - - hash2 := new(felt.Felt).SetUint64(2) - addr2 := new(felt.Felt).SetUint64(22) - - hash3 := new(felt.Felt).SetUint64(3) - hash4 := new(felt.Felt).SetUint64(4) - hash5 := new(felt.Felt).SetUint64(5) - - syncer.pendingTxs.Send([]core.Transaction{ - &core.InvokeTransaction{TransactionHash: hash1, SenderAddress: addr1}, - &core.DeclareTransaction{TransactionHash: hash2, SenderAddress: addr2}, - &core.DeployTransaction{TransactionHash: hash3}, - &core.DeployAccountTransaction{DeployTransaction: core.DeployTransaction{TransactionHash: hash4}}, - &core.L1HandlerTransaction{TransactionHash: hash5}, - }) - - want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":["0x1","0x2","0x3","0x4","0x5"],"subscription_id":%d}}` - want = fmt.Sprintf(want, id) - _, pendingTxsGot, err := conn1.Read(ctx) - require.NoError(t, err) - require.Equal(t, want, string(pendingTxsGot)) - }) - - t.Run("Filtered subscription", func(t *testing.T) { - conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) - require.NoError(t, err) - - subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"sender_address":["0xb", "0x16"]}}` - id := uint64(1) - handler.WithIDGen(func() uint64 { return id }) - got := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) - want := fmt.Sprintf(subscribeResponse, id) - require.Equal(t, want, got) - - hash1 := new(felt.Felt).SetUint64(1) - addr1 := new(felt.Felt).SetUint64(11) - - hash2 := new(felt.Felt).SetUint64(2) - addr2 := new(felt.Felt).SetUint64(22) - - hash3 := new(felt.Felt).SetUint64(3) - hash4 := new(felt.Felt).SetUint64(4) - hash5 := new(felt.Felt).SetUint64(5) - - hash6 := new(felt.Felt).SetUint64(6) - addr6 := new(felt.Felt).SetUint64(66) - - hash7 := new(felt.Felt).SetUint64(7) - addr7 := new(felt.Felt).SetUint64(77) - - syncer.pendingTxs.Send([]core.Transaction{ - &core.InvokeTransaction{TransactionHash: hash1, SenderAddress: addr1}, - &core.DeclareTransaction{TransactionHash: hash2, SenderAddress: addr2}, - &core.DeployTransaction{TransactionHash: hash3}, - &core.DeployAccountTransaction{DeployTransaction: core.DeployTransaction{TransactionHash: hash4}}, - &core.L1HandlerTransaction{TransactionHash: hash5}, - &core.InvokeTransaction{TransactionHash: hash6, SenderAddress: addr6}, - &core.DeclareTransaction{TransactionHash: hash7, SenderAddress: addr7}, - }) - - want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":["0x1","0x2"],"subscription_id":%d}}` - want = fmt.Sprintf(want, id) - _, pendingTxsGot, err := conn1.Read(ctx) - require.NoError(t, err) - require.Equal(t, want, string(pendingTxsGot)) - }) - - t.Run("Full details subscription", func(t *testing.T) { - t.Parallel() - conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) - require.NoError(t, err) - - subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"transaction_details": true}}` - id := uint64(1) - handler.WithIDGen(func() uint64 { return id }) - got := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) - want := fmt.Sprintf(subscribeResponse, id) - require.Equal(t, want, got) - - syncer.pendingTxs.Send([]core.Transaction{ - &core.InvokeTransaction{ - TransactionHash: new(felt.Felt).SetUint64(1), - CallData: []*felt.Felt{new(felt.Felt).SetUint64(2)}, - TransactionSignature: []*felt.Felt{new(felt.Felt).SetUint64(3)}, - MaxFee: new(felt.Felt).SetUint64(4), - ContractAddress: new(felt.Felt).SetUint64(5), - Version: new(core.TransactionVersion).SetUint64(3), - EntryPointSelector: new(felt.Felt).SetUint64(6), - Nonce: new(felt.Felt).SetUint64(7), - SenderAddress: new(felt.Felt).SetUint64(8), - ResourceBounds: map[core.Resource]core.ResourceBounds{}, - Tip: 9, - PaymasterData: []*felt.Felt{new(felt.Felt).SetUint64(10)}, - AccountDeploymentData: []*felt.Felt{new(felt.Felt).SetUint64(11)}, - }, - }) - - want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":[{"transaction_hash":"0x1","type":"INVOKE","version":"0x3","nonce":"0x7","max_fee":"0x4","contract_address":"0x5","sender_address":"0x8","signature":["0x3"],"calldata":["0x2"],"entry_point_selector":"0x6","resource_bounds":{},"tip":"0x9","paymaster_data":["0xa"],"account_deployment_data":["0xb"],"nonce_data_availability_mode":"L1","fee_data_availability_mode":"L1"}],"subscription_id":%d}}` - want = fmt.Sprintf(want, id) - _, pendingTxsGot, err := conn1.Read(ctx) - require.NoError(t, err) - require.Equal(t, want, string(pendingTxsGot)) - }) -} - -func testHeader(t *testing.T) *core.Header { - t.Helper() - - header := &core.Header{ - Hash: utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), - ParentHash: utils.HexToFelt(t, "0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb"), - Number: 2, - GlobalStateRoot: utils.HexToFelt(t, "0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9"), - Timestamp: 1637084470, - SequencerAddress: utils.HexToFelt(t, "0x0"), - L1DataGasPrice: &core.GasPrice{ - PriceInFri: utils.HexToFelt(t, "0x0"), - PriceInWei: utils.HexToFelt(t, "0x0"), - }, - GasPrice: utils.HexToFelt(t, "0x0"), - GasPriceSTRK: utils.HexToFelt(t, "0x0"), - L1DAMode: core.Calldata, - ProtocolVersion: "", - } - return header -} - -func setupSubscriptionTest(t *testing.T, ctx context.Context) (*rpc.Handler, *fakeSyncer, *jsonrpc.Server) { - t.Helper() - - log := utils.NewNopZapLogger() - chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) - syncer := newFakeSyncer() - handler := rpc.New(chain, syncer, nil, "", log) - - go func() { - require.NoError(t, handler.Run(ctx)) - }() - time.Sleep(50 * time.Millisecond) - - server := jsonrpc.NewServer(1, log) - methods, _ := handler.Methods() - require.NoError(t, server.RegisterMethods(methods...)) - - return handler, syncer, server -} - -func sendAndReceiveMessage(t *testing.T, ctx context.Context, conn *websocket.Conn, message string) string { - t.Helper() - - require.NoError(t, conn.Write(ctx, websocket.MessageText, []byte(message))) - - _, response, err := conn.Read(ctx) - require.NoError(t, err) - return string(response) -} diff --git a/rpc/subscriptions_test.go b/rpc/subscriptions_test.go index a3ab61fa7c..a9db089604 100644 --- a/rpc/subscriptions_test.go +++ b/rpc/subscriptions_test.go @@ -3,8 +3,10 @@ package rpc import ( "context" "encoding/json" + "fmt" "io" "net" + "net/http/httptest" "testing" "time" @@ -12,16 +14,27 @@ import ( "github.com/NethermindEth/juno/clients/feeder" "github.com/NethermindEth/juno/core" "github.com/NethermindEth/juno/core/felt" + "github.com/NethermindEth/juno/db/pebble" "github.com/NethermindEth/juno/feed" "github.com/NethermindEth/juno/jsonrpc" "github.com/NethermindEth/juno/mocks" adaptfeeder "github.com/NethermindEth/juno/starknetdata/feeder" + "github.com/NethermindEth/juno/sync" "github.com/NethermindEth/juno/utils" + "github.com/coder/websocket" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" ) +var emptyCommitments = core.BlockCommitments{} + +const ( + subscribeNewHeads = `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads"}` + newHeadsResponse = `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` + subscribeResponse = `{"jsonrpc":"2.0","result":{"subscription_id":%d},"id":1}` +) + // Due to the difference in how some test files in rpc use "package rpc" vs "package rpc_test" it was easiest to copy // the fakeConn here. // Todo: move all the subscription related test here @@ -299,6 +312,7 @@ func TestSubscribeEvents(t *testing.T) { require.Nil(t, rpcErr) resp, err := marshalSubscriptionResponse(emittedEvents[0], id.ID) + t.Log(string(resp)) require.NoError(t, err) got := make([]byte, len(resp)) @@ -326,6 +340,465 @@ func TestSubscribeEvents(t *testing.T) { }) } +type fakeSyncer struct { + newHeads *feed.Feed[*core.Header] + reorgs *feed.Feed[*sync.ReorgData] + pendingTxs *feed.Feed[[]core.Transaction] +} + +func newFakeSyncer() *fakeSyncer { + return &fakeSyncer{ + newHeads: feed.New[*core.Header](), + reorgs: feed.New[*sync.ReorgData](), + pendingTxs: feed.New[[]core.Transaction](), + } +} + +func (fs *fakeSyncer) SubscribeNewHeads() sync.HeaderSubscription { + return sync.HeaderSubscription{Subscription: fs.newHeads.Subscribe()} +} + +func (fs *fakeSyncer) SubscribeReorg() sync.ReorgSubscription { + return sync.ReorgSubscription{Subscription: fs.reorgs.Subscribe()} +} + +func (fs *fakeSyncer) SubscribePendingTxs() sync.PendingTxSubscription { + return sync.PendingTxSubscription{Subscription: fs.pendingTxs.Subscribe()} +} + +func (fs *fakeSyncer) StartingBlockNumber() (uint64, error) { + return 0, nil +} + +func (fs *fakeSyncer) HighestBlockHeader() *core.Header { + return nil +} + +func TestSubscribeNewHeads(t *testing.T) { + log := utils.NewNopZapLogger() + + t.Run("Return error if called on pending block", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + t.Cleanup(mockCtrl.Finish) + + mockChain := mocks.NewMockReader(mockCtrl) + mockSyncer := mocks.NewMockSyncReader(mockCtrl) + handler := New(mockChain, mockSyncer, nil, "", log) + + serverConn, clientConn := net.Pipe() + t.Cleanup(func() { + require.NoError(t, serverConn.Close()) + require.NoError(t, clientConn.Close()) + }) + + subCtx := context.WithValue(context.Background(), jsonrpc.ConnKey{}, &fakeConn{w: serverConn}) + + id, rpcErr := handler.SubscribeNewHeads(subCtx, &BlockID{Pending: true}) + assert.Zero(t, id) + assert.Equal(t, ErrCallOnPending, rpcErr) + }) + + t.Run("Return error if block is too far back", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + t.Cleanup(mockCtrl.Finish) + + mockChain := mocks.NewMockReader(mockCtrl) + mockSyncer := mocks.NewMockSyncReader(mockCtrl) + handler := New(mockChain, mockSyncer, nil, "", log) + + blockID := &BlockID{Number: 0} + + serverConn, clientConn := net.Pipe() + t.Cleanup(func() { + require.NoError(t, serverConn.Close()) + require.NoError(t, clientConn.Close()) + }) + + subCtx := context.WithValue(context.Background(), jsonrpc.ConnKey{}, &fakeConn{w: serverConn}) + + t.Run("head is 1024", func(t *testing.T) { + mockChain.EXPECT().HeadsHeader().Return(&core.Header{Number: 1024}, nil) + mockChain.EXPECT().BlockHeaderByNumber(blockID.Number).Return(&core.Header{Number: 0}, nil) + + id, rpcErr := handler.SubscribeNewHeads(subCtx, blockID) + assert.Zero(t, id) + assert.Equal(t, ErrTooManyBlocksBack, rpcErr) + }) + + t.Run("head is more than 1024", func(t *testing.T) { + mockChain.EXPECT().HeadsHeader().Return(&core.Header{Number: 2024}, nil) + mockChain.EXPECT().BlockHeaderByNumber(blockID.Number).Return(&core.Header{Number: 0}, nil) + + id, rpcErr := handler.SubscribeNewHeads(subCtx, blockID) + assert.Zero(t, id) + assert.Equal(t, ErrTooManyBlocksBack, rpcErr) + }) + }) + + t.Run("new block is received", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + handler, syncer, server := setupSubscriptionTest(t, ctx) + + ws := jsonrpc.NewWebsocket(server, log) + httpSrv := httptest.NewServer(ws) + + conn, _, err := websocket.Dial(ctx, httpSrv.URL, nil) + require.NoError(t, err) + + id := uint64(1) + handler.WithIDGen(func() uint64 { return id }) + + got := sendAndReceiveMessage(t, ctx, conn, subscribeNewHeads) + want := fmt.Sprintf(subscribeResponse, id) + require.Equal(t, want, got) + + // Simulate a new block + syncer.newHeads.Send(testHeader(t)) + + // Receive a block header. + want = fmt.Sprintf(newHeadsResponse, id) + _, headerGot, err := conn.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(headerGot)) + }) +} + +func TestSubscribeNewHeadsHistorical(t *testing.T) { + t.Parallel() + + log := utils.NewNopZapLogger() + client := feeder.NewTestClient(t, &utils.Mainnet) + gw := adaptfeeder.New(client) + + block0, err := gw.BlockByNumber(context.Background(), 0) + require.NoError(t, err) + + stateUpdate0, err := gw.StateUpdate(context.Background(), 0) + require.NoError(t, err) + + testDB := pebble.NewMemTest(t) + chain := blockchain.New(testDB, &utils.Mainnet) + assert.NoError(t, chain.Store(block0, &emptyCommitments, stateUpdate0, nil)) + + chain = blockchain.New(testDB, &utils.Mainnet) + syncer := newFakeSyncer() + handler := New(chain, syncer, nil, "", utils.NewNopZapLogger()) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + go func() { + require.NoError(t, handler.Run(ctx)) + }() + time.Sleep(50 * time.Millisecond) + + server := jsonrpc.NewServer(1, log) + methods, _ := handler.Methods() + require.NoError(t, server.RegisterMethods(methods...)) + + ws := jsonrpc.NewWebsocket(server, log) + httpSrv := httptest.NewServer(ws) + + conn, _, err := websocket.Dial(ctx, httpSrv.URL, nil) + require.NoError(t, err) + + id := uint64(1) + handler.WithIDGen(func() uint64 { return id }) + + subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads", "params":{"block":{"block_number":0}}}` + got := sendAndReceiveMessage(t, ctx, conn, subscribeMsg) + want := fmt.Sprintf(subscribeResponse, id) + require.NoError(t, err) + require.Equal(t, want, got) + + // Check block 0 content + want = `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x47c3637b57c2b079b93c61539950c17e868a28f46cdef28f88521067f21e943","parent_hash":"0x0","block_number":0,"new_root":"0x21870ba80540e7831fb21c591ee93481f5ae1bb71ff85a86ddd465be4eddee6","timestamp":1637069048,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` + want = fmt.Sprintf(want, id) + _, block0Got, err := conn.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(block0Got)) + + // Simulate a new block + syncer.newHeads.Send(testHeader(t)) + + // Check new block content + want = fmt.Sprintf(newHeadsResponse, id) + _, newBlockGot, err := conn.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(newBlockGot)) +} + +func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + handler, syncer, server := setupSubscriptionTest(t, ctx) + + ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) + httpSrv := httptest.NewServer(ws) + + conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) + require.NoError(t, err) + conn2, _, err := websocket.Dial(ctx, httpSrv.URL, nil) + require.NoError(t, err) + + firstID := uint64(1) + secondID := uint64(2) + + handler.WithIDGen(func() uint64 { return firstID }) + firstWant := fmt.Sprintf(subscribeResponse, firstID) + firstGot := sendAndReceiveMessage(t, ctx, conn1, subscribeNewHeads) + require.NoError(t, err) + require.Equal(t, firstWant, firstGot) + + handler.WithIDGen(func() uint64 { return secondID }) + secondWant := fmt.Sprintf(subscribeResponse, secondID) + secondGot := sendAndReceiveMessage(t, ctx, conn2, subscribeNewHeads) + require.NoError(t, err) + require.Equal(t, secondWant, secondGot) + + // Simulate a new block + syncer.newHeads.Send(testHeader(t)) + + // Receive a block header. + firstHeaderWant := fmt.Sprintf(newHeadsResponse, firstID) + _, firstHeaderGot, err := conn1.Read(ctx) + require.NoError(t, err) + require.Equal(t, firstHeaderWant, string(firstHeaderGot)) + + secondHeaderWant := fmt.Sprintf(newHeadsResponse, secondID) + _, secondHeaderGot, err := conn2.Read(ctx) + require.NoError(t, err) + require.Equal(t, secondHeaderWant, string(secondHeaderGot)) + + // Unsubscribe + unsubMsg := `{"jsonrpc":"2.0","id":1,"method":"juno_unsubscribe","params":[%d]}` + require.NoError(t, conn1.Write(ctx, websocket.MessageBinary, []byte(fmt.Sprintf(unsubMsg, firstID)))) + require.NoError(t, conn2.Write(ctx, websocket.MessageBinary, []byte(fmt.Sprintf(unsubMsg, secondID)))) +} + +func TestSubscriptionReorg(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + handler, syncer, server := setupSubscriptionTest(t, ctx) + + ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) + httpSrv := httptest.NewServer(ws) + + conn, _, err := websocket.Dial(ctx, httpSrv.URL, nil) + require.NoError(t, err) + + id := uint64(1) + handler.WithIDGen(func() uint64 { return id }) + + got := sendAndReceiveMessage(t, ctx, conn, subscribeNewHeads) + want := fmt.Sprintf(subscribeResponse, id) + require.Equal(t, want, got) + + // Simulate a reorg + syncer.reorgs.Send(&sync.ReorgData{ + StartBlockHash: utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), + StartBlockNum: 0, + EndBlockHash: utils.HexToFelt(t, "0x34e815552e42c5eb5233b99de2d3d7fd396e575df2719bf98e7ed2794494f86"), + EndBlockNum: 2, + }) + + // Receive reorg event + want = `{"jsonrpc":"2.0","method":"starknet_subscriptionReorg","params":{"result":{"starting_block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","starting_block_number":0,"ending_block_hash":"0x34e815552e42c5eb5233b99de2d3d7fd396e575df2719bf98e7ed2794494f86","ending_block_number":2},"subscription_id":%d}}` + want = fmt.Sprintf(want, id) + _, reorgGot, err := conn.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(reorgGot)) +} + +func TestSubscribePendingTxs(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + handler, syncer, server := setupSubscriptionTest(t, ctx) + + ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) + httpSrv := httptest.NewServer(ws) + + t.Run("Basic subscription", func(t *testing.T) { + conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) + require.NoError(t, err) + + subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions"}` + id := uint64(1) + handler.WithIDGen(func() uint64 { return id }) + got := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) + want := fmt.Sprintf(subscribeResponse, id) + require.Equal(t, want, got) + + hash1 := new(felt.Felt).SetUint64(1) + addr1 := new(felt.Felt).SetUint64(11) + + hash2 := new(felt.Felt).SetUint64(2) + addr2 := new(felt.Felt).SetUint64(22) + + hash3 := new(felt.Felt).SetUint64(3) + hash4 := new(felt.Felt).SetUint64(4) + hash5 := new(felt.Felt).SetUint64(5) + + syncer.pendingTxs.Send([]core.Transaction{ + &core.InvokeTransaction{TransactionHash: hash1, SenderAddress: addr1}, + &core.DeclareTransaction{TransactionHash: hash2, SenderAddress: addr2}, + &core.DeployTransaction{TransactionHash: hash3}, + &core.DeployAccountTransaction{DeployTransaction: core.DeployTransaction{TransactionHash: hash4}}, + &core.L1HandlerTransaction{TransactionHash: hash5}, + }) + + want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":["0x1","0x2","0x3","0x4","0x5"],"subscription_id":%d}}` + want = fmt.Sprintf(want, id) + _, pendingTxsGot, err := conn1.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(pendingTxsGot)) + }) + + t.Run("Filtered subscription", func(t *testing.T) { + conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) + require.NoError(t, err) + + subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"sender_address":["0xb", "0x16"]}}` + id := uint64(1) + handler.WithIDGen(func() uint64 { return id }) + got := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) + want := fmt.Sprintf(subscribeResponse, id) + require.Equal(t, want, got) + + hash1 := new(felt.Felt).SetUint64(1) + addr1 := new(felt.Felt).SetUint64(11) + + hash2 := new(felt.Felt).SetUint64(2) + addr2 := new(felt.Felt).SetUint64(22) + + hash3 := new(felt.Felt).SetUint64(3) + hash4 := new(felt.Felt).SetUint64(4) + hash5 := new(felt.Felt).SetUint64(5) + + hash6 := new(felt.Felt).SetUint64(6) + addr6 := new(felt.Felt).SetUint64(66) + + hash7 := new(felt.Felt).SetUint64(7) + addr7 := new(felt.Felt).SetUint64(77) + + syncer.pendingTxs.Send([]core.Transaction{ + &core.InvokeTransaction{TransactionHash: hash1, SenderAddress: addr1}, + &core.DeclareTransaction{TransactionHash: hash2, SenderAddress: addr2}, + &core.DeployTransaction{TransactionHash: hash3}, + &core.DeployAccountTransaction{DeployTransaction: core.DeployTransaction{TransactionHash: hash4}}, + &core.L1HandlerTransaction{TransactionHash: hash5}, + &core.InvokeTransaction{TransactionHash: hash6, SenderAddress: addr6}, + &core.DeclareTransaction{TransactionHash: hash7, SenderAddress: addr7}, + }) + + want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":["0x1","0x2"],"subscription_id":%d}}` + want = fmt.Sprintf(want, id) + _, pendingTxsGot, err := conn1.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(pendingTxsGot)) + }) + + t.Run("Full details subscription", func(t *testing.T) { + t.Parallel() + conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) + require.NoError(t, err) + + subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"transaction_details": true}}` + id := uint64(1) + handler.WithIDGen(func() uint64 { return id }) + got := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) + want := fmt.Sprintf(subscribeResponse, id) + require.Equal(t, want, got) + + syncer.pendingTxs.Send([]core.Transaction{ + &core.InvokeTransaction{ + TransactionHash: new(felt.Felt).SetUint64(1), + CallData: []*felt.Felt{new(felt.Felt).SetUint64(2)}, + TransactionSignature: []*felt.Felt{new(felt.Felt).SetUint64(3)}, + MaxFee: new(felt.Felt).SetUint64(4), + ContractAddress: new(felt.Felt).SetUint64(5), + Version: new(core.TransactionVersion).SetUint64(3), + EntryPointSelector: new(felt.Felt).SetUint64(6), + Nonce: new(felt.Felt).SetUint64(7), + SenderAddress: new(felt.Felt).SetUint64(8), + ResourceBounds: map[core.Resource]core.ResourceBounds{}, + Tip: 9, + PaymasterData: []*felt.Felt{new(felt.Felt).SetUint64(10)}, + AccountDeploymentData: []*felt.Felt{new(felt.Felt).SetUint64(11)}, + }, + }) + + want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":[{"transaction_hash":"0x1","type":"INVOKE","version":"0x3","nonce":"0x7","max_fee":"0x4","contract_address":"0x5","sender_address":"0x8","signature":["0x3"],"calldata":["0x2"],"entry_point_selector":"0x6","resource_bounds":{},"tip":"0x9","paymaster_data":["0xa"],"account_deployment_data":["0xb"],"nonce_data_availability_mode":"L1","fee_data_availability_mode":"L1"}],"subscription_id":%d}}` + want = fmt.Sprintf(want, id) + _, pendingTxsGot, err := conn1.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(pendingTxsGot)) + }) +} + +func testHeader(t *testing.T) *core.Header { + t.Helper() + + header := &core.Header{ + Hash: utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), + ParentHash: utils.HexToFelt(t, "0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb"), + Number: 2, + GlobalStateRoot: utils.HexToFelt(t, "0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9"), + Timestamp: 1637084470, + SequencerAddress: utils.HexToFelt(t, "0x0"), + L1DataGasPrice: &core.GasPrice{ + PriceInFri: utils.HexToFelt(t, "0x0"), + PriceInWei: utils.HexToFelt(t, "0x0"), + }, + GasPrice: utils.HexToFelt(t, "0x0"), + GasPriceSTRK: utils.HexToFelt(t, "0x0"), + L1DAMode: core.Calldata, + ProtocolVersion: "", + } + return header +} + +func setupSubscriptionTest(t *testing.T, ctx context.Context) (*Handler, *fakeSyncer, *jsonrpc.Server) { + t.Helper() + + log := utils.NewNopZapLogger() + chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) + syncer := newFakeSyncer() + handler := New(chain, syncer, nil, "", log) + + go func() { + require.NoError(t, handler.Run(ctx)) + }() + time.Sleep(50 * time.Millisecond) + + server := jsonrpc.NewServer(1, log) + methods, _ := handler.Methods() + require.NoError(t, server.RegisterMethods(methods...)) + + return handler, syncer, server +} + +func sendAndReceiveMessage(t *testing.T, ctx context.Context, conn *websocket.Conn, message string) string { + t.Helper() + + require.NoError(t, conn.Write(ctx, websocket.MessageText, []byte(message))) + + _, response, err := conn.Read(ctx) + require.NoError(t, err) + return string(response) +} + func marshalSubscriptionResponse(e *EmittedEvent, id uint64) ([]byte, error) { return json.Marshal(SubscriptionResponse{ Version: "2.0", From c441664b7c887f96da1727f094343b54a572f6d3 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 10:13:32 +0800 Subject: [PATCH 14/26] simplify old new heads streaming with no ordering --- rpc/events.go | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/rpc/events.go b/rpc/events.go index cff7d27902..750f5647f4 100644 --- a/rpc/events.go +++ b/rpc/events.go @@ -93,18 +93,15 @@ func (h *Handler) SubscribeNewHeads(ctx context.Context, blockID *BlockID) (*Sub var wg conc.WaitGroup - newHeadersChan := make(chan *core.Header, MaxBlocksBack) wg.Go(func() { - h.bufferNewHeaders(subscriptionCtx, headerSub, newHeadersChan) + if err := h.sendHistoricalHeaders(subscriptionCtx, startHeader, latestHeader, w, id); err != nil { + h.log.Errorw("Error sending old headers", "err", err) + return + } }) - if err := h.sendHistoricalHeaders(subscriptionCtx, startHeader, latestHeader, w, id); err != nil { - h.log.Errorw("Error sending old headers", "err", err) - return - } - wg.Go(func() { - h.processNewHeaders(subscriptionCtx, newHeadersChan, w, id) + h.processNewHeaders(subscriptionCtx, headerSub, w, id) }) wg.Go(func() { @@ -224,19 +221,18 @@ func (h *Handler) shouldIncludeTx(txn core.Transaction, senderAddr []felt.Felt) } func (h *Handler) sendPendingTxs(w jsonrpc.Conn, result interface{}, id uint64) error { - req := jsonrpc.Request{ + resp, err := json.Marshal(SubscriptionResponse{ Version: "2.0", Method: "starknet_subscriptionPendingTransactions", Params: map[string]interface{}{ "subscription_id": id, "result": result, }, - } - - resp, err := json.Marshal(req) + }) if err != nil { return err } + _, err = w.Write(resp) return err } @@ -316,12 +312,12 @@ func (h *Handler) bufferNewHeaders(ctx context.Context, headerSub *feed.Subscrip } } -func (h *Handler) processNewHeaders(ctx context.Context, newHeadersChan <-chan *core.Header, w jsonrpc.Conn, id uint64) { +func (h *Handler) processNewHeaders(ctx context.Context, headerSub *feed.Subscription[*core.Header], w jsonrpc.Conn, id uint64) { for { select { case <-ctx.Done(): return - case header := <-newHeadersChan: + case header := <-headerSub.Recv(): if err := h.sendHeader(w, header, id); err != nil { h.log.Warnw("Error sending header", "err", err) return @@ -332,7 +328,7 @@ func (h *Handler) processNewHeaders(ctx context.Context, newHeadersChan <-chan * // sendHeader creates a request and sends it to the client func (h *Handler) sendHeader(w jsonrpc.Conn, header *core.Header, id uint64) error { - resp, err := json.Marshal(jsonrpc.Request{ + resp, err := json.Marshal(SubscriptionResponse{ Version: "2.0", Method: "starknet_subscriptionNewHeads", Params: map[string]any{ From 407d82356363e8ea226d4880f54c94dba6ccb2d1 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 10:22:14 +0800 Subject: [PATCH 15/26] pending tx add error check --- rpc/events.go | 9 ++------- rpc/subscriptions_test.go | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/rpc/events.go b/rpc/events.go index 750f5647f4..1861f16880 100644 --- a/rpc/events.go +++ b/rpc/events.go @@ -13,11 +13,6 @@ import ( "github.com/sourcegraph/conc" ) -const ( - MaxBlocksBack = 1024 - MaxAddressesInFilter = 1024 // An arbitrary number, to be revisited when we have more contexts -) - type EventsArg struct { EventFilter ResultPageRequest @@ -120,7 +115,7 @@ func (h *Handler) SubscribePendingTxs(ctx context.Context, getDetails *bool, sen return nil, jsonrpc.Err(jsonrpc.MethodNotFound, nil) } - if len(senderAddr) > MaxAddressesInFilter { + if len(senderAddr) > maxEventFilterKeys { return nil, ErrTooManyAddressesInFilter } @@ -258,7 +253,7 @@ func (h *Handler) getStartAndLatestHeaders(blockID *BlockID) (*core.Header, *cor } // TODO(weiihann): also, reuse this function in other places - if latestHeader.Number >= MaxBlocksBack && startHeader.Number <= latestHeader.Number-MaxBlocksBack { + if latestHeader.Number >= maxBlocksBack && startHeader.Number <= latestHeader.Number-maxBlocksBack { return nil, nil, ErrTooManyBlocksBack } diff --git a/rpc/subscriptions_test.go b/rpc/subscriptions_test.go index a9db089604..f2ae551e4d 100644 --- a/rpc/subscriptions_test.go +++ b/rpc/subscriptions_test.go @@ -745,6 +745,25 @@ func TestSubscribePendingTxs(t *testing.T) { require.NoError(t, err) require.Equal(t, want, string(pendingTxsGot)) }) + + t.Run("Return error if too many addresses in filter", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + t.Cleanup(mockCtrl.Finish) + + addresses := make([]felt.Felt, 1024+1) + + serverConn, clientConn := net.Pipe() + t.Cleanup(func() { + require.NoError(t, serverConn.Close()) + require.NoError(t, clientConn.Close()) + }) + + subCtx := context.WithValue(context.Background(), jsonrpc.ConnKey{}, &fakeConn{w: serverConn}) + + id, rpcErr := handler.SubscribePendingTxs(subCtx, nil, addresses) + assert.Zero(t, id) + assert.Equal(t, ErrTooManyAddressesInFilter, rpcErr) + }) } func testHeader(t *testing.T) *core.Header { From 7f73ed05f4c2aea3ab958eb25756c5945d2876ed Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 12:05:57 +0800 Subject: [PATCH 16/26] all tests pass slightly cleaner --- rpc/events.go | 336 ------------------------------------- rpc/subscriptions.go | 344 ++++++++++++++++++++++++++++++++++++-- rpc/subscriptions_test.go | 68 ++++---- 3 files changed, 369 insertions(+), 379 deletions(-) diff --git a/rpc/events.go b/rpc/events.go index 1861f16880..0f403c546f 100644 --- a/rpc/events.go +++ b/rpc/events.go @@ -1,16 +1,9 @@ package rpc import ( - "context" - "encoding/json" - "github.com/NethermindEth/juno/blockchain" - "github.com/NethermindEth/juno/core" "github.com/NethermindEth/juno/core/felt" - "github.com/NethermindEth/juno/feed" "github.com/NethermindEth/juno/jsonrpc" - "github.com/NethermindEth/juno/sync" - "github.com/sourcegraph/conc" ) type EventsArg struct { @@ -55,335 +48,6 @@ type SubscriptionID struct { /**************************************************** Events Handlers *****************************************************/ - -func (h *Handler) SubscribeNewHeads(ctx context.Context, blockID *BlockID) (*SubscriptionID, *jsonrpc.Error) { - w, ok := jsonrpc.ConnFromContext(ctx) - if !ok { - return nil, jsonrpc.Err(jsonrpc.MethodNotFound, nil) - } - - startHeader, latestHeader, rpcErr := h.getStartAndLatestHeaders(blockID) - if rpcErr != nil { - return nil, rpcErr - } - - id := h.idgen() - subscriptionCtx, subscriptionCtxCancel := context.WithCancel(ctx) - sub := &subscription{ - cancel: subscriptionCtxCancel, - conn: w, - } - h.mu.Lock() - h.subscriptions[id] = sub - h.mu.Unlock() - - headerSub := h.newHeads.Subscribe() - reorgSub := h.reorgs.Subscribe() // as per the spec, reorgs are also sent in the new heads subscription - sub.wg.Go(func() { - defer func() { - h.unsubscribe(sub, id) - headerSub.Unsubscribe() - reorgSub.Unsubscribe() - }() - - var wg conc.WaitGroup - - wg.Go(func() { - if err := h.sendHistoricalHeaders(subscriptionCtx, startHeader, latestHeader, w, id); err != nil { - h.log.Errorw("Error sending old headers", "err", err) - return - } - }) - - wg.Go(func() { - h.processNewHeaders(subscriptionCtx, headerSub, w, id) - }) - - wg.Go(func() { - h.processReorgs(subscriptionCtx, reorgSub, w, id) - }) - - wg.Wait() - }) - - return &SubscriptionID{ID: id}, nil -} - -func (h *Handler) SubscribePendingTxs(ctx context.Context, getDetails *bool, senderAddr []felt.Felt) (*SubscriptionID, *jsonrpc.Error) { - w, ok := jsonrpc.ConnFromContext(ctx) - if !ok { - return nil, jsonrpc.Err(jsonrpc.MethodNotFound, nil) - } - - if len(senderAddr) > maxEventFilterKeys { - return nil, ErrTooManyAddressesInFilter - } - - id := h.idgen() - subscriptionCtx, subscriptionCtxCancel := context.WithCancel(ctx) - sub := &subscription{ - cancel: subscriptionCtxCancel, - conn: w, - } - h.mu.Lock() - h.subscriptions[id] = sub - h.mu.Unlock() - - pendingTxsSub := h.pendingTxs.Subscribe() - sub.wg.Go(func() { - defer func() { - h.unsubscribe(sub, id) - pendingTxsSub.Unsubscribe() - }() - - h.processPendingTxs(subscriptionCtx, getDetails != nil && *getDetails, senderAddr, pendingTxsSub, w, id) - }) - - return &SubscriptionID{ID: id}, nil -} - -func (h *Handler) processPendingTxs( - ctx context.Context, - getDetails bool, - senderAddr []felt.Felt, - pendingTxsSub *feed.Subscription[[]core.Transaction], - w jsonrpc.Conn, - id uint64, -) { - for { - select { - case <-ctx.Done(): - return - case pendingTxs := <-pendingTxsSub.Recv(): - filteredTxs := h.filterTxs(pendingTxs, getDetails, senderAddr) - if err := h.sendPendingTxs(w, filteredTxs, id); err != nil { - h.log.Warnw("Error sending pending transactions", "err", err) - return - } - } - } -} - -func (h *Handler) filterTxs(pendingTxs []core.Transaction, getDetails bool, senderAddr []felt.Felt) interface{} { - if getDetails { - return h.filterTxDetails(pendingTxs, senderAddr) - } - return h.filterTxHashes(pendingTxs, senderAddr) -} - -func (h *Handler) filterTxDetails(pendingTxs []core.Transaction, senderAddr []felt.Felt) []*Transaction { - filteredTxs := make([]*Transaction, 0, len(pendingTxs)) - for _, txn := range pendingTxs { - if h.shouldIncludeTx(txn, senderAddr) { - filteredTxs = append(filteredTxs, AdaptTransaction(txn)) - } - } - return filteredTxs -} - -func (h *Handler) filterTxHashes(pendingTxs []core.Transaction, senderAddr []felt.Felt) []felt.Felt { - filteredTxHashes := make([]felt.Felt, 0, len(pendingTxs)) - for _, txn := range pendingTxs { - if h.shouldIncludeTx(txn, senderAddr) { - filteredTxHashes = append(filteredTxHashes, *txn.Hash()) - } - } - return filteredTxHashes -} - -func (h *Handler) shouldIncludeTx(txn core.Transaction, senderAddr []felt.Felt) bool { - if len(senderAddr) == 0 { - return true - } - - // - switch t := txn.(type) { - case *core.InvokeTransaction: - for _, addr := range senderAddr { - if t.SenderAddress.Equal(&addr) { - return true - } - } - case *core.DeclareTransaction: - for _, addr := range senderAddr { - if t.SenderAddress.Equal(&addr) { - return true - } - } - } - - return false -} - -func (h *Handler) sendPendingTxs(w jsonrpc.Conn, result interface{}, id uint64) error { - resp, err := json.Marshal(SubscriptionResponse{ - Version: "2.0", - Method: "starknet_subscriptionPendingTransactions", - Params: map[string]interface{}{ - "subscription_id": id, - "result": result, - }, - }) - if err != nil { - return err - } - - _, err = w.Write(resp) - return err -} - -// getStartAndLatestHeaders gets the start and latest header for the subscription -func (h *Handler) getStartAndLatestHeaders(blockID *BlockID) (*core.Header, *core.Header, *jsonrpc.Error) { - if blockID == nil || blockID.Latest { - return nil, nil, nil - } - - if blockID.Pending { - return nil, nil, ErrCallOnPending - } - - latestHeader, err := h.bcReader.HeadsHeader() - if err != nil { - return nil, nil, ErrInternal - } - - startHeader, rpcErr := h.blockHeaderByID(blockID) - if rpcErr != nil { - return nil, nil, rpcErr - } - - // TODO(weiihann): also, reuse this function in other places - if latestHeader.Number >= maxBlocksBack && startHeader.Number <= latestHeader.Number-maxBlocksBack { - return nil, nil, ErrTooManyBlocksBack - } - - return startHeader, latestHeader, nil -} - -// sendHistoricalHeaders sends a range of headers from the start header until the latest header -func (h *Handler) sendHistoricalHeaders( - ctx context.Context, - startHeader *core.Header, - latestHeader *core.Header, - w jsonrpc.Conn, - id uint64, -) error { - if startHeader == nil { - return nil - } - - var err error - - lastHeader := startHeader - for { - select { - case <-ctx.Done(): - return ctx.Err() - default: - if err := h.sendHeader(w, lastHeader, id); err != nil { - return err - } - - if lastHeader.Number == latestHeader.Number { - return nil - } - - lastHeader, err = h.bcReader.BlockHeaderByNumber(lastHeader.Number + 1) - if err != nil { - return err - } - } - } -} - -func (h *Handler) bufferNewHeaders(ctx context.Context, headerSub *feed.Subscription[*core.Header], newHeadersChan chan<- *core.Header) { - for { - select { - case <-ctx.Done(): - return - case header := <-headerSub.Recv(): - newHeadersChan <- header - } - } -} - -func (h *Handler) processNewHeaders(ctx context.Context, headerSub *feed.Subscription[*core.Header], w jsonrpc.Conn, id uint64) { - for { - select { - case <-ctx.Done(): - return - case header := <-headerSub.Recv(): - if err := h.sendHeader(w, header, id); err != nil { - h.log.Warnw("Error sending header", "err", err) - return - } - } - } -} - -// sendHeader creates a request and sends it to the client -func (h *Handler) sendHeader(w jsonrpc.Conn, header *core.Header, id uint64) error { - resp, err := json.Marshal(SubscriptionResponse{ - Version: "2.0", - Method: "starknet_subscriptionNewHeads", - Params: map[string]any{ - "subscription_id": id, - "result": adaptBlockHeader(header), - }, - }) - if err != nil { - return err - } - _, err = w.Write(resp) - return err -} - -func (h *Handler) processReorgs(ctx context.Context, reorgSub *feed.Subscription[*sync.ReorgData], w jsonrpc.Conn, id uint64) { - for { - select { - case <-ctx.Done(): - return - case reorg := <-reorgSub.Recv(): - if err := h.sendReorg(w, reorg, id); err != nil { - h.log.Warnw("Error sending reorg", "err", err) - return - } - } - } -} - -func (h *Handler) sendReorg(w jsonrpc.Conn, reorg *sync.ReorgData, id uint64) error { - resp, err := json.Marshal(jsonrpc.Request{ - Version: "2.0", - Method: "starknet_subscriptionReorg", - Params: map[string]any{ - "subscription_id": id, - "result": reorg, - }, - }) - if err != nil { - return err - } - _, err = w.Write(resp) - return err -} - -func (h *Handler) Unsubscribe(ctx context.Context, id uint64) (bool, *jsonrpc.Error) { - w, ok := jsonrpc.ConnFromContext(ctx) - if !ok { - return false, jsonrpc.Err(jsonrpc.MethodNotFound, nil) - } - h.mu.Lock() - sub, ok := h.subscriptions[id] - h.mu.Unlock() // Don't defer since h.unsubscribe acquires the lock. - if !ok || !sub.conn.Equal(w) { - return false, ErrSubscriptionNotFound - } - sub.cancel() - sub.wg.Wait() // Let the subscription finish before responding. - return true, nil -} - // Events gets the events matching a filter // // It follows the specification defined here: diff --git a/rpc/subscriptions.go b/rpc/subscriptions.go index b049c6ce0d..9d97ceb0b0 100644 --- a/rpc/subscriptions.go +++ b/rpc/subscriptions.go @@ -3,12 +3,14 @@ package rpc import ( "context" "encoding/json" - "sync" "github.com/NethermindEth/juno/blockchain" "github.com/NethermindEth/juno/core" "github.com/NethermindEth/juno/core/felt" + "github.com/NethermindEth/juno/feed" "github.com/NethermindEth/juno/jsonrpc" + "github.com/NethermindEth/juno/sync" + "github.com/sourcegraph/conc" ) const subscribeEventsChunkSize = 1024 @@ -70,33 +72,35 @@ func (h *Handler) SubscribeEvents(ctx context.Context, fromAddr *felt.Felt, keys h.mu.Unlock() headerSub := h.newHeads.Subscribe() + reorgSub := h.reorgs.Subscribe() // as per the spec, reorgs are also sent in the events subscription sub.wg.Go(func() { defer func() { h.unsubscribe(sub, id) headerSub.Unsubscribe() + reorgSub.Unsubscribe() }() // The specification doesn't enforce ordering of events therefore events from new blocks can be sent before // old blocks. - // Todo: see if sub's wg can be used? - wg := sync.WaitGroup{} - wg.Add(1) - - go func() { - defer wg.Done() - + var wg conc.WaitGroup + wg.Go(func() { for { select { case <-subscriptionCtx.Done(): return case header := <-headerSub.Recv(): - h.processEvents(subscriptionCtx, w, id, header.Number, header.Number, fromAddr, keys) } } - }() + }) + + wg.Go(func() { + h.processReorgs(subscriptionCtx, reorgSub, w, id) + }) - h.processEvents(subscriptionCtx, w, id, requestedHeader.Number, headHeader.Number, fromAddr, keys) + wg.Go(func() { + h.processEvents(subscriptionCtx, w, id, requestedHeader.Number, headHeader.Number, fromAddr, keys) + }) wg.Wait() }) @@ -182,3 +186,321 @@ func sendEvents(ctx context.Context, w jsonrpc.Conn, events []*blockchain.Filter } return nil } + +func (h *Handler) SubscribeNewHeads(ctx context.Context, blockID *BlockID) (*SubscriptionID, *jsonrpc.Error) { + w, ok := jsonrpc.ConnFromContext(ctx) + if !ok { + return nil, jsonrpc.Err(jsonrpc.MethodNotFound, nil) + } + + startHeader, latestHeader, rpcErr := h.getStartAndLatestHeaders(blockID) + if rpcErr != nil { + return nil, rpcErr + } + + id := h.idgen() + subscriptionCtx, subscriptionCtxCancel := context.WithCancel(ctx) + sub := &subscription{ + cancel: subscriptionCtxCancel, + conn: w, + } + h.mu.Lock() + h.subscriptions[id] = sub + h.mu.Unlock() + + headerSub := h.newHeads.Subscribe() + reorgSub := h.reorgs.Subscribe() // as per the spec, reorgs are also sent in the new heads subscription + sub.wg.Go(func() { + defer func() { + h.unsubscribe(sub, id) + headerSub.Unsubscribe() + reorgSub.Unsubscribe() + }() + + var wg conc.WaitGroup + + wg.Go(func() { + if err := h.sendHistoricalHeaders(subscriptionCtx, startHeader, latestHeader, w, id); err != nil { + h.log.Errorw("Error sending old headers", "err", err) + return + } + }) + + wg.Go(func() { + h.processReorgs(subscriptionCtx, reorgSub, w, id) + }) + + wg.Go(func() { + h.processNewHeaders(subscriptionCtx, headerSub, w, id) + }) + + wg.Wait() + }) + + return &SubscriptionID{ID: id}, nil +} + +func (h *Handler) SubscribePendingTxs(ctx context.Context, getDetails *bool, senderAddr []felt.Felt) (*SubscriptionID, *jsonrpc.Error) { + w, ok := jsonrpc.ConnFromContext(ctx) + if !ok { + return nil, jsonrpc.Err(jsonrpc.MethodNotFound, nil) + } + + if len(senderAddr) > maxEventFilterKeys { + return nil, ErrTooManyAddressesInFilter + } + + id := h.idgen() + subscriptionCtx, subscriptionCtxCancel := context.WithCancel(ctx) + sub := &subscription{ + cancel: subscriptionCtxCancel, + conn: w, + } + h.mu.Lock() + h.subscriptions[id] = sub + h.mu.Unlock() + + pendingTxsSub := h.pendingTxs.Subscribe() + sub.wg.Go(func() { + defer func() { + h.unsubscribe(sub, id) + pendingTxsSub.Unsubscribe() + }() + + h.processPendingTxs(subscriptionCtx, getDetails != nil && *getDetails, senderAddr, pendingTxsSub, w, id) + }) + + return &SubscriptionID{ID: id}, nil +} + +func (h *Handler) processPendingTxs( + ctx context.Context, + getDetails bool, + senderAddr []felt.Felt, + pendingTxsSub *feed.Subscription[[]core.Transaction], + w jsonrpc.Conn, + id uint64, +) { + for { + select { + case <-ctx.Done(): + return + case pendingTxs := <-pendingTxsSub.Recv(): + filteredTxs := h.filterTxs(pendingTxs, getDetails, senderAddr) + if err := h.sendPendingTxs(w, filteredTxs, id); err != nil { + h.log.Warnw("Error sending pending transactions", "err", err) + return + } + } + } +} + +func (h *Handler) filterTxs(pendingTxs []core.Transaction, getDetails bool, senderAddr []felt.Felt) interface{} { + if getDetails { + return h.filterTxDetails(pendingTxs, senderAddr) + } + return h.filterTxHashes(pendingTxs, senderAddr) +} + +func (h *Handler) filterTxDetails(pendingTxs []core.Transaction, senderAddr []felt.Felt) []*Transaction { + filteredTxs := make([]*Transaction, 0, len(pendingTxs)) + for _, txn := range pendingTxs { + if h.shouldIncludeTx(txn, senderAddr) { + filteredTxs = append(filteredTxs, AdaptTransaction(txn)) + } + } + return filteredTxs +} + +func (h *Handler) filterTxHashes(pendingTxs []core.Transaction, senderAddr []felt.Felt) []felt.Felt { + filteredTxHashes := make([]felt.Felt, 0, len(pendingTxs)) + for _, txn := range pendingTxs { + if h.shouldIncludeTx(txn, senderAddr) { + filteredTxHashes = append(filteredTxHashes, *txn.Hash()) + } + } + return filteredTxHashes +} + +func (h *Handler) shouldIncludeTx(txn core.Transaction, senderAddr []felt.Felt) bool { + if len(senderAddr) == 0 { + return true + } + + switch t := txn.(type) { + case *core.InvokeTransaction: + for _, addr := range senderAddr { + if t.SenderAddress.Equal(&addr) { + return true + } + } + case *core.DeclareTransaction: + for _, addr := range senderAddr { + if t.SenderAddress.Equal(&addr) { + return true + } + } + } + + return false +} + +func (h *Handler) sendPendingTxs(w jsonrpc.Conn, result interface{}, id uint64) error { + resp, err := json.Marshal(SubscriptionResponse{ + Version: "2.0", + Method: "starknet_subscriptionPendingTransactions", + Params: map[string]interface{}{ + "subscription_id": id, + "result": result, + }, + }) + if err != nil { + return err + } + + _, err = w.Write(resp) + return err +} + +// getStartAndLatestHeaders returns the start and latest headers based on the blockID. +// It will also do some sanity checks and return errors if the blockID is invalid. +func (h *Handler) getStartAndLatestHeaders(blockID *BlockID) (*core.Header, *core.Header, *jsonrpc.Error) { + latestHeader, err := h.bcReader.HeadsHeader() + if err != nil { + return nil, nil, ErrInternal.CloneWithData(err.Error()) + } + + if blockID == nil || blockID.Latest { + return latestHeader, latestHeader, nil + } + + if blockID.Pending { + return nil, nil, ErrCallOnPending + } + + startHeader, rpcErr := h.blockHeaderByID(blockID) + if rpcErr != nil { + return nil, nil, rpcErr + } + + if latestHeader.Number >= maxBlocksBack && startHeader.Number <= latestHeader.Number-maxBlocksBack { + return nil, nil, ErrTooManyBlocksBack + } + + return startHeader, latestHeader, nil +} + +// sendHistoricalHeaders sends a range of headers from the start header until the latest header +func (h *Handler) sendHistoricalHeaders( + ctx context.Context, + startHeader *core.Header, + latestHeader *core.Header, + w jsonrpc.Conn, + id uint64, +) error { + if startHeader == latestHeader { + return nil + } + + var ( + err error + curHeader = startHeader + ) + + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + if err := h.sendHeader(w, curHeader, id); err != nil { + return err + } + + if curHeader.Number == latestHeader.Number { + return nil + } + + curHeader, err = h.bcReader.BlockHeaderByNumber(curHeader.Number + 1) + if err != nil { + return err + } + } + } +} + +func (h *Handler) processNewHeaders(ctx context.Context, headerSub *feed.Subscription[*core.Header], w jsonrpc.Conn, id uint64) { + for { + select { + case <-ctx.Done(): + return + case header := <-headerSub.Recv(): + if err := h.sendHeader(w, header, id); err != nil { + h.log.Warnw("Error sending header", "err", err) + return + } + } + } +} + +// sendHeader creates a request and sends it to the client +func (h *Handler) sendHeader(w jsonrpc.Conn, header *core.Header, id uint64) error { + resp, err := json.Marshal(SubscriptionResponse{ + Version: "2.0", + Method: "starknet_subscriptionNewHeads", + Params: map[string]any{ + "subscription_id": id, + "result": adaptBlockHeader(header), + }, + }) + if err != nil { + return err + } + _, err = w.Write(resp) + return err +} + +func (h *Handler) processReorgs(ctx context.Context, reorgSub *feed.Subscription[*sync.ReorgData], w jsonrpc.Conn, id uint64) { + for { + select { + case <-ctx.Done(): + return + case reorg := <-reorgSub.Recv(): + if err := h.sendReorg(w, reorg, id); err != nil { + h.log.Warnw("Error sending reorg", "err", err) + return + } + } + } +} + +func (h *Handler) sendReorg(w jsonrpc.Conn, reorg *sync.ReorgData, id uint64) error { + resp, err := json.Marshal(jsonrpc.Request{ + Version: "2.0", + Method: "starknet_subscriptionReorg", + Params: map[string]any{ + "subscription_id": id, + "result": reorg, + }, + }) + if err != nil { + return err + } + _, err = w.Write(resp) + return err +} + +func (h *Handler) Unsubscribe(ctx context.Context, id uint64) (bool, *jsonrpc.Error) { + w, ok := jsonrpc.ConnFromContext(ctx) + if !ok { + return false, jsonrpc.Err(jsonrpc.MethodNotFound, nil) + } + h.mu.Lock() + sub, ok := h.subscriptions[id] + h.mu.Unlock() // Don't defer since h.unsubscribe acquires the lock. + if !ok || !sub.conn.Equal(w) { + return false, ErrSubscriptionNotFound + } + sub.cancel() + sub.wg.Wait() // Let the subscription finish before responding. + return true, nil +} diff --git a/rpc/subscriptions_test.go b/rpc/subscriptions_test.go index f2ae551e4d..d8ac44c3ad 100644 --- a/rpc/subscriptions_test.go +++ b/rpc/subscriptions_test.go @@ -312,7 +312,6 @@ func TestSubscribeEvents(t *testing.T) { require.Nil(t, rpcErr) resp, err := marshalSubscriptionResponse(emittedEvents[0], id.ID) - t.Log(string(resp)) require.NoError(t, err) got := make([]byte, len(resp)) @@ -385,6 +384,8 @@ func TestSubscribeNewHeads(t *testing.T) { mockSyncer := mocks.NewMockSyncReader(mockCtrl) handler := New(mockChain, mockSyncer, nil, "", log) + mockChain.EXPECT().HeadsHeader().Return(&core.Header{}, nil) + serverConn, clientConn := net.Pipe() t.Cleanup(func() { require.NoError(t, serverConn.Close()) @@ -436,10 +437,16 @@ func TestSubscribeNewHeads(t *testing.T) { }) t.Run("new block is received", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + t.Cleanup(mockCtrl.Finish) + ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - handler, syncer, server := setupSubscriptionTest(t, ctx) + mockChain := mocks.NewMockReader(mockCtrl) + mockChain.EXPECT().HeadsHeader().Return(&core.Header{}, nil) + syncer := newFakeSyncer() + handler, server := setupSubscriptionTest(t, ctx, mockChain, syncer) ws := jsonrpc.NewWebsocket(server, log) httpSrv := httptest.NewServer(ws) @@ -466,8 +473,6 @@ func TestSubscribeNewHeads(t *testing.T) { } func TestSubscribeNewHeadsHistorical(t *testing.T) { - t.Parallel() - log := utils.NewNopZapLogger() client := feeder.NewTestClient(t, &utils.Mainnet) gw := adaptfeeder.New(client) @@ -484,19 +489,11 @@ func TestSubscribeNewHeadsHistorical(t *testing.T) { chain = blockchain.New(testDB, &utils.Mainnet) syncer := newFakeSyncer() - handler := New(chain, syncer, nil, "", utils.NewNopZapLogger()) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - go func() { - require.NoError(t, handler.Run(ctx)) - }() - time.Sleep(50 * time.Millisecond) - - server := jsonrpc.NewServer(1, log) - methods, _ := handler.Methods() - require.NoError(t, server.RegisterMethods(methods...)) + handler, server := setupSubscriptionTest(t, ctx, chain, syncer) ws := jsonrpc.NewWebsocket(server, log) httpSrv := httptest.NewServer(ws) @@ -531,12 +528,17 @@ func TestSubscribeNewHeadsHistorical(t *testing.T) { } func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { - t.Parallel() + mockCtrl := gomock.NewController(t) + t.Cleanup(mockCtrl.Finish) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - handler, syncer, server := setupSubscriptionTest(t, ctx) + mockChain := mocks.NewMockReader(mockCtrl) + syncer := newFakeSyncer() + handler, server := setupSubscriptionTest(t, ctx, mockChain, syncer) + + mockChain.EXPECT().HeadsHeader().Return(&core.Header{}, nil).Times(2) ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) httpSrv := httptest.NewServer(ws) @@ -582,12 +584,17 @@ func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { } func TestSubscriptionReorg(t *testing.T) { - t.Parallel() - ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - handler, syncer, server := setupSubscriptionTest(t, ctx) + mockCtrl := gomock.NewController(t) + t.Cleanup(mockCtrl.Finish) + + mockChain := mocks.NewMockReader(mockCtrl) + syncer := newFakeSyncer() + handler, server := setupSubscriptionTest(t, ctx, mockChain, syncer) + + mockChain.EXPECT().HeadsHeader().Return(&core.Header{}, nil) ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) httpSrv := httptest.NewServer(ws) @@ -619,12 +626,15 @@ func TestSubscriptionReorg(t *testing.T) { } func TestSubscribePendingTxs(t *testing.T) { - t.Parallel() - ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - handler, syncer, server := setupSubscriptionTest(t, ctx) + mockCtrl := gomock.NewController(t) + t.Cleanup(mockCtrl.Finish) + + mockChain := mocks.NewMockReader(mockCtrl) + syncer := newFakeSyncer() + handler, server := setupSubscriptionTest(t, ctx, mockChain, syncer) ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) httpSrv := httptest.NewServer(ws) @@ -710,7 +720,6 @@ func TestSubscribePendingTxs(t *testing.T) { }) t.Run("Full details subscription", func(t *testing.T) { - t.Parallel() conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) require.NoError(t, err) @@ -747,15 +756,11 @@ func TestSubscribePendingTxs(t *testing.T) { }) t.Run("Return error if too many addresses in filter", func(t *testing.T) { - mockCtrl := gomock.NewController(t) - t.Cleanup(mockCtrl.Finish) - addresses := make([]felt.Felt, 1024+1) - serverConn, clientConn := net.Pipe() + serverConn, _ := net.Pipe() t.Cleanup(func() { require.NoError(t, serverConn.Close()) - require.NoError(t, clientConn.Close()) }) subCtx := context.WithValue(context.Background(), jsonrpc.ConnKey{}, &fakeConn{w: serverConn}) @@ -788,12 +793,10 @@ func testHeader(t *testing.T) *core.Header { return header } -func setupSubscriptionTest(t *testing.T, ctx context.Context) (*Handler, *fakeSyncer, *jsonrpc.Server) { +func setupSubscriptionTest(t *testing.T, ctx context.Context, chain blockchain.Reader, syncer sync.Reader) (*Handler, *jsonrpc.Server) { t.Helper() log := utils.NewNopZapLogger() - chain := blockchain.New(pebble.NewMemTest(t), &utils.Mainnet) - syncer := newFakeSyncer() handler := New(chain, syncer, nil, "", log) go func() { @@ -805,13 +808,14 @@ func setupSubscriptionTest(t *testing.T, ctx context.Context) (*Handler, *fakeSy methods, _ := handler.Methods() require.NoError(t, server.RegisterMethods(methods...)) - return handler, syncer, server + return handler, server } func sendAndReceiveMessage(t *testing.T, ctx context.Context, conn *websocket.Conn, message string) string { t.Helper() - require.NoError(t, conn.Write(ctx, websocket.MessageText, []byte(message))) + err := conn.Write(ctx, websocket.MessageText, []byte(message)) + require.NoError(t, err) _, response, err := conn.Read(ctx) require.NoError(t, err) From bb45626017ff27cee3662116032754f6a0a92af7 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 12:18:19 +0800 Subject: [PATCH 17/26] slightly cleaner --- rpc/subscriptions_test.go | 82 ++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/rpc/subscriptions_test.go b/rpc/subscriptions_test.go index d8ac44c3ad..c816f2ac0b 100644 --- a/rpc/subscriptions_test.go +++ b/rpc/subscriptions_test.go @@ -444,15 +444,12 @@ func TestSubscribeNewHeads(t *testing.T) { t.Cleanup(cancel) mockChain := mocks.NewMockReader(mockCtrl) - mockChain.EXPECT().HeadsHeader().Return(&core.Header{}, nil) syncer := newFakeSyncer() - handler, server := setupSubscriptionTest(t, ctx, mockChain, syncer) + handler, server := setupRPC(t, ctx, mockChain, syncer) - ws := jsonrpc.NewWebsocket(server, log) - httpSrv := httptest.NewServer(ws) + mockChain.EXPECT().HeadsHeader().Return(&core.Header{}, nil) - conn, _, err := websocket.Dial(ctx, httpSrv.URL, nil) - require.NoError(t, err) + conn := createWsConn(t, ctx, server) id := uint64(1) handler.WithIDGen(func() uint64 { return id }) @@ -473,7 +470,6 @@ func TestSubscribeNewHeads(t *testing.T) { } func TestSubscribeNewHeadsHistorical(t *testing.T) { - log := utils.NewNopZapLogger() client := feeder.NewTestClient(t, &utils.Mainnet) gw := adaptfeeder.New(client) @@ -493,13 +489,9 @@ func TestSubscribeNewHeadsHistorical(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - handler, server := setupSubscriptionTest(t, ctx, chain, syncer) + handler, server := setupRPC(t, ctx, chain, syncer) - ws := jsonrpc.NewWebsocket(server, log) - httpSrv := httptest.NewServer(ws) - - conn, _, err := websocket.Dial(ctx, httpSrv.URL, nil) - require.NoError(t, err) + conn := createWsConn(t, ctx, server) id := uint64(1) handler.WithIDGen(func() uint64 { return id }) @@ -536,17 +528,24 @@ func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { mockChain := mocks.NewMockReader(mockCtrl) syncer := newFakeSyncer() - handler, server := setupSubscriptionTest(t, ctx, mockChain, syncer) + handler, server := setupRPC(t, ctx, mockChain, syncer) mockChain.EXPECT().HeadsHeader().Return(&core.Header{}, nil).Times(2) ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) httpSrv := httptest.NewServer(ws) - conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) + conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) //nolint:bodyclose require.NoError(t, err) - conn2, _, err := websocket.Dial(ctx, httpSrv.URL, nil) + t.Cleanup(func() { + require.NoError(t, conn1.Close(websocket.StatusNormalClosure, "")) + }) + + conn2, _, err := websocket.Dial(ctx, httpSrv.URL, nil) //nolint:bodyclose require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, conn2.Close(websocket.StatusNormalClosure, "")) + }) firstID := uint64(1) secondID := uint64(2) @@ -592,15 +591,11 @@ func TestSubscriptionReorg(t *testing.T) { mockChain := mocks.NewMockReader(mockCtrl) syncer := newFakeSyncer() - handler, server := setupSubscriptionTest(t, ctx, mockChain, syncer) + handler, server := setupRPC(t, ctx, mockChain, syncer) mockChain.EXPECT().HeadsHeader().Return(&core.Header{}, nil) - ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) - httpSrv := httptest.NewServer(ws) - - conn, _, err := websocket.Dial(ctx, httpSrv.URL, nil) - require.NoError(t, err) + conn := createWsConn(t, ctx, server) id := uint64(1) handler.WithIDGen(func() uint64 { return id }) @@ -634,19 +629,15 @@ func TestSubscribePendingTxs(t *testing.T) { mockChain := mocks.NewMockReader(mockCtrl) syncer := newFakeSyncer() - handler, server := setupSubscriptionTest(t, ctx, mockChain, syncer) - - ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) - httpSrv := httptest.NewServer(ws) + handler, server := setupRPC(t, ctx, mockChain, syncer) t.Run("Basic subscription", func(t *testing.T) { - conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) - require.NoError(t, err) + conn := createWsConn(t, ctx, server) subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions"}` id := uint64(1) handler.WithIDGen(func() uint64 { return id }) - got := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) + got := sendAndReceiveMessage(t, ctx, conn, subscribeMsg) want := fmt.Sprintf(subscribeResponse, id) require.Equal(t, want, got) @@ -670,19 +661,18 @@ func TestSubscribePendingTxs(t *testing.T) { want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":["0x1","0x2","0x3","0x4","0x5"],"subscription_id":%d}}` want = fmt.Sprintf(want, id) - _, pendingTxsGot, err := conn1.Read(ctx) + _, pendingTxsGot, err := conn.Read(ctx) require.NoError(t, err) require.Equal(t, want, string(pendingTxsGot)) }) t.Run("Filtered subscription", func(t *testing.T) { - conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) - require.NoError(t, err) + conn := createWsConn(t, ctx, server) subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"sender_address":["0xb", "0x16"]}}` id := uint64(1) handler.WithIDGen(func() uint64 { return id }) - got := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) + got := sendAndReceiveMessage(t, ctx, conn, subscribeMsg) want := fmt.Sprintf(subscribeResponse, id) require.Equal(t, want, got) @@ -714,19 +704,18 @@ func TestSubscribePendingTxs(t *testing.T) { want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":["0x1","0x2"],"subscription_id":%d}}` want = fmt.Sprintf(want, id) - _, pendingTxsGot, err := conn1.Read(ctx) + _, pendingTxsGot, err := conn.Read(ctx) require.NoError(t, err) require.Equal(t, want, string(pendingTxsGot)) }) t.Run("Full details subscription", func(t *testing.T) { - conn1, _, err := websocket.Dial(ctx, httpSrv.URL, nil) - require.NoError(t, err) + conn := createWsConn(t, ctx, server) subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"transaction_details": true}}` id := uint64(1) handler.WithIDGen(func() uint64 { return id }) - got := sendAndReceiveMessage(t, ctx, conn1, subscribeMsg) + got := sendAndReceiveMessage(t, ctx, conn, subscribeMsg) want := fmt.Sprintf(subscribeResponse, id) require.Equal(t, want, got) @@ -750,7 +739,7 @@ func TestSubscribePendingTxs(t *testing.T) { want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":[{"transaction_hash":"0x1","type":"INVOKE","version":"0x3","nonce":"0x7","max_fee":"0x4","contract_address":"0x5","sender_address":"0x8","signature":["0x3"],"calldata":["0x2"],"entry_point_selector":"0x6","resource_bounds":{},"tip":"0x9","paymaster_data":["0xa"],"account_deployment_data":["0xb"],"nonce_data_availability_mode":"L1","fee_data_availability_mode":"L1"}],"subscription_id":%d}}` want = fmt.Sprintf(want, id) - _, pendingTxsGot, err := conn1.Read(ctx) + _, pendingTxsGot, err := conn.Read(ctx) require.NoError(t, err) require.Equal(t, want, string(pendingTxsGot)) }) @@ -771,6 +760,20 @@ func TestSubscribePendingTxs(t *testing.T) { }) } +func createWsConn(t *testing.T, ctx context.Context, server *jsonrpc.Server) *websocket.Conn { + ws := jsonrpc.NewWebsocket(server, utils.NewNopZapLogger()) + httpSrv := httptest.NewServer(ws) + + conn, _, err := websocket.Dial(ctx, httpSrv.URL, nil) //nolint:bodyclose + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, conn.Close(websocket.StatusNormalClosure, "")) + }) + + return conn +} + func testHeader(t *testing.T) *core.Header { t.Helper() @@ -793,7 +796,8 @@ func testHeader(t *testing.T) *core.Header { return header } -func setupSubscriptionTest(t *testing.T, ctx context.Context, chain blockchain.Reader, syncer sync.Reader) (*Handler, *jsonrpc.Server) { +// setupRPC creates a RPC handler that runs in a goroutine and a JSONRPC server that can be used to test subscriptions +func setupRPC(t *testing.T, ctx context.Context, chain blockchain.Reader, syncer sync.Reader) (*Handler, *jsonrpc.Server) { t.Helper() log := utils.NewNopZapLogger() From 3876ecfe4444e6bbe74e3c58e774eb0de248e7e5 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 12:28:44 +0800 Subject: [PATCH 18/26] minor change --- rpc/subscriptions_test.go | 63 +++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/rpc/subscriptions_test.go b/rpc/subscriptions_test.go index c816f2ac0b..e53c867a39 100644 --- a/rpc/subscriptions_test.go +++ b/rpc/subscriptions_test.go @@ -68,10 +68,9 @@ func TestSubscribeEvents(t *testing.T) { keys := make([][]felt.Felt, 1024+1) fromAddr := new(felt.Felt).SetBytes([]byte("from_address")) - serverConn, clientConn := net.Pipe() + serverConn, _ := net.Pipe() t.Cleanup(func() { require.NoError(t, serverConn.Close()) - require.NoError(t, clientConn.Close()) }) subCtx := context.WithValue(context.Background(), jsonrpc.ConnKey{}, &fakeConn{w: serverConn}) @@ -93,10 +92,9 @@ func TestSubscribeEvents(t *testing.T) { fromAddr := new(felt.Felt).SetBytes([]byte("from_address")) blockID := &BlockID{Pending: true} - serverConn, clientConn := net.Pipe() + serverConn, _ := net.Pipe() t.Cleanup(func() { require.NoError(t, serverConn.Close()) - require.NoError(t, clientConn.Close()) }) subCtx := context.WithValue(context.Background(), jsonrpc.ConnKey{}, &fakeConn{w: serverConn}) @@ -120,10 +118,9 @@ func TestSubscribeEvents(t *testing.T) { fromAddr := new(felt.Felt).SetBytes([]byte("from_address")) blockID := &BlockID{Number: 0} - serverConn, clientConn := net.Pipe() + serverConn, _ := net.Pipe() t.Cleanup(func() { require.NoError(t, serverConn.Close()) - require.NoError(t, clientConn.Close()) }) subCtx := context.WithValue(context.Background(), jsonrpc.ConnKey{}, &fakeConn{w: serverConn}) @@ -386,10 +383,9 @@ func TestSubscribeNewHeads(t *testing.T) { mockChain.EXPECT().HeadsHeader().Return(&core.Header{}, nil) - serverConn, clientConn := net.Pipe() + serverConn, _ := net.Pipe() t.Cleanup(func() { require.NoError(t, serverConn.Close()) - require.NoError(t, clientConn.Close()) }) subCtx := context.WithValue(context.Background(), jsonrpc.ConnKey{}, &fakeConn{w: serverConn}) @@ -409,10 +405,9 @@ func TestSubscribeNewHeads(t *testing.T) { blockID := &BlockID{Number: 0} - serverConn, clientConn := net.Pipe() + serverConn, _ := net.Pipe() t.Cleanup(func() { require.NoError(t, serverConn.Close()) - require.NoError(t, clientConn.Close()) }) subCtx := context.WithValue(context.Background(), jsonrpc.ConnKey{}, &fakeConn{w: serverConn}) @@ -589,35 +584,37 @@ func TestSubscriptionReorg(t *testing.T) { mockCtrl := gomock.NewController(t) t.Cleanup(mockCtrl.Finish) - mockChain := mocks.NewMockReader(mockCtrl) - syncer := newFakeSyncer() - handler, server := setupRPC(t, ctx, mockChain, syncer) + t.Run("reorg event in starknet_subscribeNewHeads", func(t *testing.T) { + mockChain := mocks.NewMockReader(mockCtrl) + syncer := newFakeSyncer() + handler, server := setupRPC(t, ctx, mockChain, syncer) - mockChain.EXPECT().HeadsHeader().Return(&core.Header{}, nil) + mockChain.EXPECT().HeadsHeader().Return(&core.Header{}, nil) - conn := createWsConn(t, ctx, server) + conn := createWsConn(t, ctx, server) - id := uint64(1) - handler.WithIDGen(func() uint64 { return id }) + id := uint64(1) + handler.WithIDGen(func() uint64 { return id }) - got := sendAndReceiveMessage(t, ctx, conn, subscribeNewHeads) - want := fmt.Sprintf(subscribeResponse, id) - require.Equal(t, want, got) + got := sendAndReceiveMessage(t, ctx, conn, subscribeNewHeads) + want := fmt.Sprintf(subscribeResponse, id) + require.Equal(t, want, got) - // Simulate a reorg - syncer.reorgs.Send(&sync.ReorgData{ - StartBlockHash: utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), - StartBlockNum: 0, - EndBlockHash: utils.HexToFelt(t, "0x34e815552e42c5eb5233b99de2d3d7fd396e575df2719bf98e7ed2794494f86"), - EndBlockNum: 2, - }) + // Simulate a reorg + syncer.reorgs.Send(&sync.ReorgData{ + StartBlockHash: utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), + StartBlockNum: 0, + EndBlockHash: utils.HexToFelt(t, "0x34e815552e42c5eb5233b99de2d3d7fd396e575df2719bf98e7ed2794494f86"), + EndBlockNum: 2, + }) - // Receive reorg event - want = `{"jsonrpc":"2.0","method":"starknet_subscriptionReorg","params":{"result":{"starting_block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","starting_block_number":0,"ending_block_hash":"0x34e815552e42c5eb5233b99de2d3d7fd396e575df2719bf98e7ed2794494f86","ending_block_number":2},"subscription_id":%d}}` - want = fmt.Sprintf(want, id) - _, reorgGot, err := conn.Read(ctx) - require.NoError(t, err) - require.Equal(t, want, string(reorgGot)) + // Receive reorg event + want = `{"jsonrpc":"2.0","method":"starknet_subscriptionReorg","params":{"result":{"starting_block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","starting_block_number":0,"ending_block_hash":"0x34e815552e42c5eb5233b99de2d3d7fd396e575df2719bf98e7ed2794494f86","ending_block_number":2},"subscription_id":%d}}` + want = fmt.Sprintf(want, id) + _, reorgGot, err := conn.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(reorgGot)) + }) } func TestSubscribePendingTxs(t *testing.T) { From cd2971cd0be8296b80a848e168f7ced005995380 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 13:00:26 +0800 Subject: [PATCH 19/26] more clean ups --- rpc/subscriptions_test.go | 161 ++++++++++++++++++++------------------ 1 file changed, 86 insertions(+), 75 deletions(-) diff --git a/rpc/subscriptions_test.go b/rpc/subscriptions_test.go index e53c867a39..e92d3206da 100644 --- a/rpc/subscriptions_test.go +++ b/rpc/subscriptions_test.go @@ -29,12 +29,6 @@ import ( var emptyCommitments = core.BlockCommitments{} -const ( - subscribeNewHeads = `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads"}` - newHeadsResponse = `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` - subscribeResponse = `{"jsonrpc":"2.0","result":{"subscription_id":%d},"id":1}` -) - // Due to the difference in how some test files in rpc use "package rpc" vs "package rpc_test" it was easiest to copy // the fakeConn here. // Todo: move all the subscription related test here @@ -216,7 +210,7 @@ func TestSubscribeEvents(t *testing.T) { var marshalledResponses [][]byte for _, e := range emittedEvents { - resp, err := marshalSubscriptionResponse(e, id.ID) + resp, err := marshalSubEventsResp(e, id.ID) require.NoError(t, err) marshalledResponses = append(marshalledResponses, resp) } @@ -264,7 +258,7 @@ func TestSubscribeEvents(t *testing.T) { var marshalledResponses [][]byte for _, e := range emittedEvents { - resp, err := marshalSubscriptionResponse(e, id.ID) + resp, err := marshalSubEventsResp(e, id.ID) require.NoError(t, err) marshalledResponses = append(marshalledResponses, resp) } @@ -308,7 +302,7 @@ func TestSubscribeEvents(t *testing.T) { id, rpcErr := handler.SubscribeEvents(subCtx, fromAddr, keys, nil) require.Nil(t, rpcErr) - resp, err := marshalSubscriptionResponse(emittedEvents[0], id.ID) + resp, err := marshalSubEventsResp(emittedEvents[0], id.ID) require.NoError(t, err) got := make([]byte, len(resp)) @@ -323,7 +317,7 @@ func TestSubscribeEvents(t *testing.T) { headerFeed.Send(&core.Header{Number: b1.Number + 1}) - resp, err = marshalSubscriptionResponse(emittedEvents[1], id.ID) + resp, err = marshalSubEventsResp(emittedEvents[1], id.ID) require.NoError(t, err) got = make([]byte, len(resp)) @@ -449,18 +443,16 @@ func TestSubscribeNewHeads(t *testing.T) { id := uint64(1) handler.WithIDGen(func() uint64 { return id }) - got := sendAndReceiveMessage(t, ctx, conn, subscribeNewHeads) - want := fmt.Sprintf(subscribeResponse, id) - require.Equal(t, want, got) + got := sendWsMessage(t, ctx, conn, subMsg("starknet_subscribeNewHeads")) + require.Equal(t, subResp(id), got) // Simulate a new block syncer.newHeads.Send(testHeader(t)) // Receive a block header. - want = fmt.Sprintf(newHeadsResponse, id) _, headerGot, err := conn.Read(ctx) require.NoError(t, err) - require.Equal(t, want, string(headerGot)) + require.Equal(t, newHeadsResponse(id), string(headerGot)) }) } @@ -491,14 +483,12 @@ func TestSubscribeNewHeadsHistorical(t *testing.T) { id := uint64(1) handler.WithIDGen(func() uint64 { return id }) - subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads", "params":{"block":{"block_number":0}}}` - got := sendAndReceiveMessage(t, ctx, conn, subscribeMsg) - want := fmt.Sprintf(subscribeResponse, id) - require.NoError(t, err) - require.Equal(t, want, got) + subMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribeNewHeads", "params":{"block":{"block_number":0}}}` + got := sendWsMessage(t, ctx, conn, subMsg) + require.Equal(t, subResp(id), got) // Check block 0 content - want = `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x47c3637b57c2b079b93c61539950c17e868a28f46cdef28f88521067f21e943","parent_hash":"0x0","block_number":0,"new_root":"0x21870ba80540e7831fb21c591ee93481f5ae1bb71ff85a86ddd465be4eddee6","timestamp":1637069048,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` + want := `{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x47c3637b57c2b079b93c61539950c17e868a28f46cdef28f88521067f21e943","parent_hash":"0x0","block_number":0,"new_root":"0x21870ba80540e7831fb21c591ee93481f5ae1bb71ff85a86ddd465be4eddee6","timestamp":1637069048,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}` want = fmt.Sprintf(want, id) _, block0Got, err := conn.Read(ctx) require.NoError(t, err) @@ -508,10 +498,9 @@ func TestSubscribeNewHeadsHistorical(t *testing.T) { syncer.newHeads.Send(testHeader(t)) // Check new block content - want = fmt.Sprintf(newHeadsResponse, id) _, newBlockGot, err := conn.Read(ctx) require.NoError(t, err) - require.Equal(t, want, string(newBlockGot)) + require.Equal(t, newHeadsResponse(id), string(newBlockGot)) } func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { @@ -546,30 +535,26 @@ func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { secondID := uint64(2) handler.WithIDGen(func() uint64 { return firstID }) - firstWant := fmt.Sprintf(subscribeResponse, firstID) - firstGot := sendAndReceiveMessage(t, ctx, conn1, subscribeNewHeads) + firstGot := sendWsMessage(t, ctx, conn1, subMsg("starknet_subscribeNewHeads")) require.NoError(t, err) - require.Equal(t, firstWant, firstGot) + require.Equal(t, subResp(firstID), firstGot) handler.WithIDGen(func() uint64 { return secondID }) - secondWant := fmt.Sprintf(subscribeResponse, secondID) - secondGot := sendAndReceiveMessage(t, ctx, conn2, subscribeNewHeads) + secondGot := sendWsMessage(t, ctx, conn2, subMsg("starknet_subscribeNewHeads")) require.NoError(t, err) - require.Equal(t, secondWant, secondGot) + require.Equal(t, subResp(secondID), secondGot) // Simulate a new block syncer.newHeads.Send(testHeader(t)) // Receive a block header. - firstHeaderWant := fmt.Sprintf(newHeadsResponse, firstID) _, firstHeaderGot, err := conn1.Read(ctx) require.NoError(t, err) - require.Equal(t, firstHeaderWant, string(firstHeaderGot)) + require.Equal(t, newHeadsResponse(firstID), string(firstHeaderGot)) - secondHeaderWant := fmt.Sprintf(newHeadsResponse, secondID) _, secondHeaderGot, err := conn2.Read(ctx) require.NoError(t, err) - require.Equal(t, secondHeaderWant, string(secondHeaderGot)) + require.Equal(t, newHeadsResponse(secondID), string(secondHeaderGot)) // Unsubscribe unsubMsg := `{"jsonrpc":"2.0","id":1,"method":"juno_unsubscribe","params":[%d]}` @@ -584,37 +569,53 @@ func TestSubscriptionReorg(t *testing.T) { mockCtrl := gomock.NewController(t) t.Cleanup(mockCtrl.Finish) - t.Run("reorg event in starknet_subscribeNewHeads", func(t *testing.T) { - mockChain := mocks.NewMockReader(mockCtrl) - syncer := newFakeSyncer() - handler, server := setupRPC(t, ctx, mockChain, syncer) + mockChain := mocks.NewMockReader(mockCtrl) + syncer := newFakeSyncer() + handler, server := setupRPC(t, ctx, mockChain, syncer) - mockChain.EXPECT().HeadsHeader().Return(&core.Header{}, nil) + testCases := []struct { + name string + subscribeMethod string + }{ + { + name: "reorg event in starknet_subscribeNewHeads", + subscribeMethod: "starknet_subscribeNewHeads", + }, + { + name: "reorg event in starknet_subscribeEvents", + subscribeMethod: "starknet_subscribeEvents", + }, + // TODO: test reorg event in TransactionStatus + } - conn := createWsConn(t, ctx, server) + mockChain.EXPECT().HeadsHeader().Return(&core.Header{}, nil).Times(len(testCases)) - id := uint64(1) - handler.WithIDGen(func() uint64 { return id }) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + conn := createWsConn(t, ctx, server) - got := sendAndReceiveMessage(t, ctx, conn, subscribeNewHeads) - want := fmt.Sprintf(subscribeResponse, id) - require.Equal(t, want, got) + id := uint64(1) + handler.WithIDGen(func() uint64 { return id }) - // Simulate a reorg - syncer.reorgs.Send(&sync.ReorgData{ - StartBlockHash: utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), - StartBlockNum: 0, - EndBlockHash: utils.HexToFelt(t, "0x34e815552e42c5eb5233b99de2d3d7fd396e575df2719bf98e7ed2794494f86"), - EndBlockNum: 2, - }) + got := sendWsMessage(t, ctx, conn, subMsg(tc.subscribeMethod)) + require.Equal(t, subResp(id), got) - // Receive reorg event - want = `{"jsonrpc":"2.0","method":"starknet_subscriptionReorg","params":{"result":{"starting_block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","starting_block_number":0,"ending_block_hash":"0x34e815552e42c5eb5233b99de2d3d7fd396e575df2719bf98e7ed2794494f86","ending_block_number":2},"subscription_id":%d}}` - want = fmt.Sprintf(want, id) - _, reorgGot, err := conn.Read(ctx) - require.NoError(t, err) - require.Equal(t, want, string(reorgGot)) - }) + // Simulate a reorg + syncer.reorgs.Send(&sync.ReorgData{ + StartBlockHash: utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), + StartBlockNum: 0, + EndBlockHash: utils.HexToFelt(t, "0x34e815552e42c5eb5233b99de2d3d7fd396e575df2719bf98e7ed2794494f86"), + EndBlockNum: 2, + }) + + // Receive reorg event + expectedRes := `{"jsonrpc":"2.0","method":"starknet_subscriptionReorg","params":{"result":{"starting_block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","starting_block_number":0,"ending_block_hash":"0x34e815552e42c5eb5233b99de2d3d7fd396e575df2719bf98e7ed2794494f86","ending_block_number":2},"subscription_id":%d}}` + want := fmt.Sprintf(expectedRes, id) + _, reorgGot, err := conn.Read(ctx) + require.NoError(t, err) + require.Equal(t, want, string(reorgGot)) + }) + } } func TestSubscribePendingTxs(t *testing.T) { @@ -631,12 +632,11 @@ func TestSubscribePendingTxs(t *testing.T) { t.Run("Basic subscription", func(t *testing.T) { conn := createWsConn(t, ctx, server) - subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions"}` + subMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions"}` id := uint64(1) handler.WithIDGen(func() uint64 { return id }) - got := sendAndReceiveMessage(t, ctx, conn, subscribeMsg) - want := fmt.Sprintf(subscribeResponse, id) - require.Equal(t, want, got) + got := sendWsMessage(t, ctx, conn, subMsg) + require.Equal(t, subResp(id), got) hash1 := new(felt.Felt).SetUint64(1) addr1 := new(felt.Felt).SetUint64(11) @@ -656,7 +656,7 @@ func TestSubscribePendingTxs(t *testing.T) { &core.L1HandlerTransaction{TransactionHash: hash5}, }) - want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":["0x1","0x2","0x3","0x4","0x5"],"subscription_id":%d}}` + want := `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":["0x1","0x2","0x3","0x4","0x5"],"subscription_id":%d}}` want = fmt.Sprintf(want, id) _, pendingTxsGot, err := conn.Read(ctx) require.NoError(t, err) @@ -666,12 +666,11 @@ func TestSubscribePendingTxs(t *testing.T) { t.Run("Filtered subscription", func(t *testing.T) { conn := createWsConn(t, ctx, server) - subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"sender_address":["0xb", "0x16"]}}` + subMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"sender_address":["0xb", "0x16"]}}` id := uint64(1) handler.WithIDGen(func() uint64 { return id }) - got := sendAndReceiveMessage(t, ctx, conn, subscribeMsg) - want := fmt.Sprintf(subscribeResponse, id) - require.Equal(t, want, got) + got := sendWsMessage(t, ctx, conn, subMsg) + require.Equal(t, subResp(id), got) hash1 := new(felt.Felt).SetUint64(1) addr1 := new(felt.Felt).SetUint64(11) @@ -699,7 +698,7 @@ func TestSubscribePendingTxs(t *testing.T) { &core.DeclareTransaction{TransactionHash: hash7, SenderAddress: addr7}, }) - want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":["0x1","0x2"],"subscription_id":%d}}` + want := `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":["0x1","0x2"],"subscription_id":%d}}` want = fmt.Sprintf(want, id) _, pendingTxsGot, err := conn.Read(ctx) require.NoError(t, err) @@ -709,12 +708,11 @@ func TestSubscribePendingTxs(t *testing.T) { t.Run("Full details subscription", func(t *testing.T) { conn := createWsConn(t, ctx, server) - subscribeMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"transaction_details": true}}` + subMsg := `{"jsonrpc":"2.0","id":1,"method":"starknet_subscribePendingTransactions", "params":{"transaction_details": true}}` id := uint64(1) handler.WithIDGen(func() uint64 { return id }) - got := sendAndReceiveMessage(t, ctx, conn, subscribeMsg) - want := fmt.Sprintf(subscribeResponse, id) - require.Equal(t, want, got) + got := sendWsMessage(t, ctx, conn, subMsg) + require.Equal(t, subResp(id), got) syncer.pendingTxs.Send([]core.Transaction{ &core.InvokeTransaction{ @@ -734,7 +732,7 @@ func TestSubscribePendingTxs(t *testing.T) { }, }) - want = `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":[{"transaction_hash":"0x1","type":"INVOKE","version":"0x3","nonce":"0x7","max_fee":"0x4","contract_address":"0x5","sender_address":"0x8","signature":["0x3"],"calldata":["0x2"],"entry_point_selector":"0x6","resource_bounds":{},"tip":"0x9","paymaster_data":["0xa"],"account_deployment_data":["0xb"],"nonce_data_availability_mode":"L1","fee_data_availability_mode":"L1"}],"subscription_id":%d}}` + want := `{"jsonrpc":"2.0","method":"starknet_subscriptionPendingTransactions","params":{"result":[{"transaction_hash":"0x1","type":"INVOKE","version":"0x3","nonce":"0x7","max_fee":"0x4","contract_address":"0x5","sender_address":"0x8","signature":["0x3"],"calldata":["0x2"],"entry_point_selector":"0x6","resource_bounds":{},"tip":"0x9","paymaster_data":["0xa"],"account_deployment_data":["0xb"],"nonce_data_availability_mode":"L1","fee_data_availability_mode":"L1"}],"subscription_id":%d}}` want = fmt.Sprintf(want, id) _, pendingTxsGot, err := conn.Read(ctx) require.NoError(t, err) @@ -771,6 +769,14 @@ func createWsConn(t *testing.T, ctx context.Context, server *jsonrpc.Server) *we return conn } +func subResp(id uint64) string { + return fmt.Sprintf(`{"jsonrpc":"2.0","result":{"subscription_id":%d},"id":1}`, id) +} + +func subMsg(method string) string { + return fmt.Sprintf(`{"jsonrpc":"2.0","id":1,"method":%q}`, method) +} + func testHeader(t *testing.T) *core.Header { t.Helper() @@ -793,6 +799,10 @@ func testHeader(t *testing.T) *core.Header { return header } +func newHeadsResponse(id uint64) string { + return fmt.Sprintf(`{"jsonrpc":"2.0","method":"starknet_subscriptionNewHeads","params":{"result":{"block_hash":"0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6","parent_hash":"0x2a70fb03fe363a2d6be843343a1d81ce6abeda1e9bd5cc6ad8fa9f45e30fdeb","block_number":2,"new_root":"0x3ceee867d50b5926bb88c0ec7e0b9c20ae6b537e74aac44b8fcf6bb6da138d9","timestamp":1637084470,"sequencer_address":"0x0","l1_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_data_gas_price":{"price_in_fri":"0x0","price_in_wei":"0x0"},"l1_da_mode":"CALLDATA","starknet_version":""},"subscription_id":%d}}`, id) +} + // setupRPC creates a RPC handler that runs in a goroutine and a JSONRPC server that can be used to test subscriptions func setupRPC(t *testing.T, ctx context.Context, chain blockchain.Reader, syncer sync.Reader) (*Handler, *jsonrpc.Server) { t.Helper() @@ -812,7 +822,8 @@ func setupRPC(t *testing.T, ctx context.Context, chain blockchain.Reader, syncer return handler, server } -func sendAndReceiveMessage(t *testing.T, ctx context.Context, conn *websocket.Conn, message string) string { +// sendWsMessage sends a message to a websocket connection and returns the response +func sendWsMessage(t *testing.T, ctx context.Context, conn *websocket.Conn, message string) string { t.Helper() err := conn.Write(ctx, websocket.MessageText, []byte(message)) @@ -823,7 +834,7 @@ func sendAndReceiveMessage(t *testing.T, ctx context.Context, conn *websocket.Co return string(response) } -func marshalSubscriptionResponse(e *EmittedEvent, id uint64) ([]byte, error) { +func marshalSubEventsResp(e *EmittedEvent, id uint64) ([]byte, error) { return json.Marshal(SubscriptionResponse{ Version: "2.0", Method: "starknet_subscriptionEvents", From 99d942b97f63aff848a401df5cc05d8536b914d2 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 13:15:08 +0800 Subject: [PATCH 20/26] ignore first header in tests --- rpc/subscriptions.go | 4 ---- rpc/subscriptions_test.go | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/rpc/subscriptions.go b/rpc/subscriptions.go index 9d97ceb0b0..ad6877dcf1 100644 --- a/rpc/subscriptions.go +++ b/rpc/subscriptions.go @@ -398,10 +398,6 @@ func (h *Handler) sendHistoricalHeaders( w jsonrpc.Conn, id uint64, ) error { - if startHeader == latestHeader { - return nil - } - var ( err error curHeader = startHeader diff --git a/rpc/subscriptions_test.go b/rpc/subscriptions_test.go index e92d3206da..e524302924 100644 --- a/rpc/subscriptions_test.go +++ b/rpc/subscriptions_test.go @@ -446,6 +446,10 @@ func TestSubscribeNewHeads(t *testing.T) { got := sendWsMessage(t, ctx, conn, subMsg("starknet_subscribeNewHeads")) require.Equal(t, subResp(id), got) + // Ignore the first mock header + _, _, err := conn.Read(ctx) + require.NoError(t, err) + // Simulate a new block syncer.newHeads.Send(testHeader(t)) @@ -544,6 +548,12 @@ func TestMultipleSubscribeNewHeadsAndUnsubscribe(t *testing.T) { require.NoError(t, err) require.Equal(t, subResp(secondID), secondGot) + // Ignore the first mock header + _, _, err = conn1.Read(ctx) + require.NoError(t, err) + _, _, err = conn2.Read(ctx) + require.NoError(t, err) + // Simulate a new block syncer.newHeads.Send(testHeader(t)) @@ -576,14 +586,17 @@ func TestSubscriptionReorg(t *testing.T) { testCases := []struct { name string subscribeMethod string + ignoreFirst bool }{ { name: "reorg event in starknet_subscribeNewHeads", subscribeMethod: "starknet_subscribeNewHeads", + ignoreFirst: true, }, { name: "reorg event in starknet_subscribeEvents", subscribeMethod: "starknet_subscribeEvents", + ignoreFirst: false, }, // TODO: test reorg event in TransactionStatus } @@ -600,6 +613,11 @@ func TestSubscriptionReorg(t *testing.T) { got := sendWsMessage(t, ctx, conn, subMsg(tc.subscribeMethod)) require.Equal(t, subResp(id), got) + if tc.ignoreFirst { + _, _, err := conn.Read(ctx) + require.NoError(t, err) + } + // Simulate a reorg syncer.reorgs.Send(&sync.ReorgData{ StartBlockHash: utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), From 81cf80cfd5cabd8ca52700193630f3496f932269 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 13:26:31 +0800 Subject: [PATCH 21/26] add docs --- rpc/subscriptions.go | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/rpc/subscriptions.go b/rpc/subscriptions.go index ad6877dcf1..0dac69df83 100644 --- a/rpc/subscriptions.go +++ b/rpc/subscriptions.go @@ -21,6 +21,7 @@ type SubscriptionResponse struct { Params any `json:"params"` } +// SubscribeEvents creates a WebSocket stream which will fire events for new Starknet events with applied filters func (h *Handler) SubscribeEvents(ctx context.Context, fromAddr *felt.Felt, keys [][]felt.Felt, blockID *BlockID, ) (*SubscriptionID, *jsonrpc.Error) { @@ -187,13 +188,14 @@ func sendEvents(ctx context.Context, w jsonrpc.Conn, events []*blockchain.Filter return nil } +// SubscribeNewHeads creates a WebSocket stream which will fire events when a new block header is added. func (h *Handler) SubscribeNewHeads(ctx context.Context, blockID *BlockID) (*SubscriptionID, *jsonrpc.Error) { w, ok := jsonrpc.ConnFromContext(ctx) if !ok { return nil, jsonrpc.Err(jsonrpc.MethodNotFound, nil) } - startHeader, latestHeader, rpcErr := h.getStartAndLatestHeaders(blockID) + startHeader, latestHeader, rpcErr := h.resolveBlockRange(blockID) if rpcErr != nil { return nil, rpcErr } @@ -240,6 +242,9 @@ func (h *Handler) SubscribeNewHeads(ctx context.Context, blockID *BlockID) (*Sub return &SubscriptionID{ID: id}, nil } +// SubscribePendingTxs creates a WebSocket stream which will fire events when a new pending transaction is added. +// The getDetails flag controls if the response will contain the transaction details or just the transaction hashes. +// The senderAddr flag is used to filter the transactions by sender address. func (h *Handler) SubscribePendingTxs(ctx context.Context, getDetails *bool, senderAddr []felt.Felt) (*SubscriptionID, *jsonrpc.Error) { w, ok := jsonrpc.ConnFromContext(ctx) if !ok { @@ -295,6 +300,9 @@ func (h *Handler) processPendingTxs( } } +// filterTxs filters the transactions based on the getDetails flag. +// If getDetails is true, response will contain the transaction details. +// If getDetails is false, response will only contain the transaction hashes. func (h *Handler) filterTxs(pendingTxs []core.Transaction, getDetails bool, senderAddr []felt.Felt) interface{} { if getDetails { return h.filterTxDetails(pendingTxs, senderAddr) @@ -305,7 +313,7 @@ func (h *Handler) filterTxs(pendingTxs []core.Transaction, getDetails bool, send func (h *Handler) filterTxDetails(pendingTxs []core.Transaction, senderAddr []felt.Felt) []*Transaction { filteredTxs := make([]*Transaction, 0, len(pendingTxs)) for _, txn := range pendingTxs { - if h.shouldIncludeTx(txn, senderAddr) { + if h.filterTxBySender(txn, senderAddr) { filteredTxs = append(filteredTxs, AdaptTransaction(txn)) } } @@ -315,14 +323,18 @@ func (h *Handler) filterTxDetails(pendingTxs []core.Transaction, senderAddr []fe func (h *Handler) filterTxHashes(pendingTxs []core.Transaction, senderAddr []felt.Felt) []felt.Felt { filteredTxHashes := make([]felt.Felt, 0, len(pendingTxs)) for _, txn := range pendingTxs { - if h.shouldIncludeTx(txn, senderAddr) { + if h.filterTxBySender(txn, senderAddr) { filteredTxHashes = append(filteredTxHashes, *txn.Hash()) } } return filteredTxHashes } -func (h *Handler) shouldIncludeTx(txn core.Transaction, senderAddr []felt.Felt) bool { +// filterTxBySender checks if the transaction is included in the sender address list. +// If the sender address list is empty, it will return true by default. +// If the sender address list is not empty, it will check if the transaction is an Invoke or Declare transaction +// and if the sender address is in the list. For other transaction types, it will by default return false. +func (h *Handler) filterTxBySender(txn core.Transaction, senderAddr []felt.Felt) bool { if len(senderAddr) == 0 { return true } @@ -362,9 +374,9 @@ func (h *Handler) sendPendingTxs(w jsonrpc.Conn, result interface{}, id uint64) return err } -// getStartAndLatestHeaders returns the start and latest headers based on the blockID. +// resolveBlockRange returns the start and latest headers based on the blockID. // It will also do some sanity checks and return errors if the blockID is invalid. -func (h *Handler) getStartAndLatestHeaders(blockID *BlockID) (*core.Header, *core.Header, *jsonrpc.Error) { +func (h *Handler) resolveBlockRange(blockID *BlockID) (*core.Header, *core.Header, *jsonrpc.Error) { latestHeader, err := h.bcReader.HeadsHeader() if err != nil { return nil, nil, ErrInternal.CloneWithData(err.Error()) From 77808115d9d397b2aaa7b70f9dc1b47d00591438 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 13:28:39 +0800 Subject: [PATCH 22/26] use resolveBlockRange --- rpc/subscriptions.go | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/rpc/subscriptions.go b/rpc/subscriptions.go index 0dac69df83..5f50500499 100644 --- a/rpc/subscriptions.go +++ b/rpc/subscriptions.go @@ -38,28 +38,9 @@ func (h *Handler) SubscribeEvents(ctx context.Context, fromAddr *felt.Felt, keys return nil, ErrTooManyKeysInFilter } - var requestedHeader *core.Header - headHeader, err := h.bcReader.HeadsHeader() - if err != nil { - return nil, ErrInternal.CloneWithData(err.Error()) - } - - if blockID == nil { - requestedHeader = headHeader - } else { - if blockID.Pending { - return nil, ErrCallOnPending - } - - var rpcErr *jsonrpc.Error - requestedHeader, rpcErr = h.blockHeaderByID(blockID) - if rpcErr != nil { - return nil, rpcErr - } - - if headHeader.Number >= maxBlocksBack && requestedHeader.Number <= headHeader.Number-maxBlocksBack { - return nil, ErrTooManyBlocksBack - } + requestedHeader, headHeader, rpcErr := h.resolveBlockRange(blockID) + if rpcErr != nil { + return nil, rpcErr } id := h.idgen() From 61cb0f6af598c8447ebff7a1592f94f54f425210 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 13:34:04 +0800 Subject: [PATCH 23/26] fix lint --- rpc/events_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/rpc/events_test.go b/rpc/events_test.go index 34b96faf91..e54765de2d 100644 --- a/rpc/events_test.go +++ b/rpc/events_test.go @@ -9,7 +9,6 @@ import ( "github.com/NethermindEth/juno/core" "github.com/NethermindEth/juno/core/felt" "github.com/NethermindEth/juno/db/pebble" - "github.com/NethermindEth/juno/rpc" adaptfeeder "github.com/NethermindEth/juno/starknetdata/feeder" "github.com/NethermindEth/juno/utils" From 105255a8c04947897123fa82d6f99c17ac4c78b6 Mon Sep 17 00:00:00 2001 From: weiihann Date: Thu, 12 Dec 2024 23:36:18 +0800 Subject: [PATCH 24/26] Squashed commit of the following: commit 76873600e9cd8b0c01f3ff77e971ba8df3a6b840 Author: Ng Wei Han <47109095+weiihann@users.noreply.github.com> Date: Thu Dec 12 18:54:32 2024 +0800 Remove size in OrderedSet (#2319) commit 65b7507fda8a8e0ee1442c9eb044ccb646979804 Author: Ng Wei Han <47109095+weiihann@users.noreply.github.com> Date: Thu Dec 12 18:20:55 2024 +0800 Fix and refactor trie proof logics (#2252) commit 2b1b21977a7df072bebfc5cf22886b871e5cc262 Author: aleven1999 Date: Thu Dec 12 12:11:28 2024 +0400 Remove unused code (#2318) commit 0a21162f7f5a06951f95f5d4c7a748361cd3b29c Author: Daniil Ankushin Date: Thu Dec 12 00:04:08 2024 +0700 Remove unused code (#2317) commit 8bf9be9fe9ac4d1dc279dd77bdd4c2e7c5028a4a Author: Rian Hughes Date: Wed Dec 11 14:11:22 2024 +0200 update invoke v3 txn validation to require sender_address (#2308) commit 91d0f8e87c454d989273022ffc43d6a4040b71e2 Author: Kirill Date: Wed Dec 11 16:01:10 2024 +0400 Add schema_version to output of db info command (#2309) commit 60e8cc9472f6eb79b7dc0021c7413b88ae7f3948 Author: AnavarKh <108727035+AnavarKh@users.noreply.github.com> Date: Wed Dec 11 16:04:31 2024 +0530 Update download link for Juno snapshots from dev to io in Readme file (#2314) commit 8862de1088a2e98c1bd018f799e12b6c96200c80 Author: wojciechos Date: Wed Dec 11 11:20:02 2024 +0100 Improve binary build workflow for cross-platform releases (#2315) - Add proper architecture handling in matrix configuration - Implement caching for Go modules and Rust dependencies - Streamline dependency installation for both Linux and macOS - Improve binary artifact handling and checksums - Add retention policy for build artifacts - Split build steps for better clarity and maintainability This update ensures more reliable and efficient binary builds across all supported platforms. commit e75e504eea82d633fa6ff063fbbe036452a11674 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed Dec 11 07:35:16 2024 +0000 Bump nanoid from 3.3.7 to 3.3.8 in /docs in the npm_and_yarn group across 1 directory (#2316) Bump nanoid in /docs in the npm_and_yarn group across 1 directory Bumps the npm_and_yarn group with 1 update in the /docs directory: [nanoid](https://github.com/ai/nanoid). Updates `nanoid` from 3.3.7 to 3.3.8 - [Release notes](https://github.com/ai/nanoid/releases) - [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md) - [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8) --- updated-dependencies: - dependency-name: nanoid dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit 3a7abeb0cd4a6e3b99dce36aafe3951add501b7a Author: wojciechos Date: Tue Dec 10 21:52:49 2024 +0100 Skip error logs for FGW responses with NOT_RECEIVED status (#2303) * Add NotReceived case handling in adaptTransactionStatus --------- Co-authored-by: Rian Hughes --- cmd/juno/dbcmd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/juno/dbcmd.go b/cmd/juno/dbcmd.go index d9892b6be1..6f6afbbdda 100644 --- a/cmd/juno/dbcmd.go +++ b/cmd/juno/dbcmd.go @@ -85,7 +85,7 @@ func dbInfo(cmd *cobra.Command, args []string) error { } defer database.Close() - chain := blockchain.New(database, nil, nil) + chain := blockchain.New(database, nil) var info DBInfo // Get the latest block information From e44dd934c00ba11b7841d250c5931c128ed4d45e Mon Sep 17 00:00:00 2001 From: weiihann Date: Fri, 13 Dec 2024 10:05:56 +0800 Subject: [PATCH 25/26] Squashed commit of the following: commit 4ff174d8cfdfbacfc119430ae56d289fa305f3d6 Author: IronGauntlets Date: Fri Apr 26 22:10:44 2024 +0100 Move pending to sync package In order to moved handling of pending to synchroniser the following changes needed to be made: - Add database to synchroniser, so that pending state can be served - Blockchain and Events Filter have a pendingBlockFn() which returns the pending block. Due to import cycle pending struct could not be referenced, therefore, the anonymous function is passed. - Add PendingBlock() to return just the pending block, this was mainly added to support the pendingBlockFn(). - In rpc package the pending block and state is retrieved through synchroniser. Therefore, receipt and transaction handler now check the pending block for the requested transaction/receipt. commit fb75cd65e05f1ebcecdd525e540fd58153cf6a11 Author: IronGauntlets Date: Fri Apr 26 15:18:33 2024 +0100 Rename cachedPending to pending commit 3ffc458fe597853734fc6a3c81c13b6bad36ee48 Author: IronGauntlets Date: Fri Apr 26 01:27:53 2024 +0100 Check pending block protocol version before storing commit 317ca386122a04622571034a582f92e22b8619db Author: IronGauntlets Date: Fri Apr 26 00:52:59 2024 +0100 Refactor blockchain.Pending to return a reference commit 03f4bfabc20512e63cba2c4c7b9f8f2c78fe5ea4 Author: IronGauntlets Date: Mon Apr 22 21:16:22 2024 +0100 Remove pending and empty pending from DB Pending Block is now only managed in memory this is to make sure that pending block in the DB and in memory do not become out of sync. Before the pending block was managed in memory as a cache, however, since there is only one pending block at a given time it doesn't make sense to keep track of pending block in both memory and DB. To reduce the number of block not found errors while simulating transactions it was decided to store empty pending block, using the latest header to fill in fields such as block number, parent block hash, etc. This meant that any time we didn't have a pending block this empty pending block would be served along with empty state diff and classes. Every time a new block was added to the blockchain a new empty pending block was also added to the DB. The unforeseen side effect of this change was when the --poll-pending-interval flag was disabled the rpc would still serve a pending block. This is incorrect behaviour. As the blocks changed per new versions of starknet the empty block also needed to be changed and a storage diff with a special contract "0x1" needed to be updated in the state diff. This overhead is unnecessary and incorrectly informs the user that there is a pending block when there isn't one. --- cmd/juno/dbcmd.go | 2 +- rpc/subscriptions_test.go | 8 +++- sync/sync.go | 91 +++++++++++++++++++++++++++++++++++++++ sync/sync_test.go | 30 +++++++++++-- 4 files changed, 124 insertions(+), 7 deletions(-) diff --git a/cmd/juno/dbcmd.go b/cmd/juno/dbcmd.go index 6f6afbbdda..d9892b6be1 100644 --- a/cmd/juno/dbcmd.go +++ b/cmd/juno/dbcmd.go @@ -85,7 +85,7 @@ func dbInfo(cmd *cobra.Command, args []string) error { } defer database.Close() - chain := blockchain.New(database, nil) + chain := blockchain.New(database, nil, nil) var info DBInfo // Get the latest block information diff --git a/rpc/subscriptions_test.go b/rpc/subscriptions_test.go index e524302924..a58b68bd72 100644 --- a/rpc/subscriptions_test.go +++ b/rpc/subscriptions_test.go @@ -364,6 +364,10 @@ func (fs *fakeSyncer) HighestBlockHeader() *core.Header { return nil } +func (fs *fakeSyncer) Pending() (*sync.Pending, error) { return nil, nil } +func (fs *fakeSyncer) PendingBlock() *core.Block { return nil } +func (fs *fakeSyncer) PendingState() (core.StateReader, func() error, error) { return nil, nil, nil } + func TestSubscribeNewHeads(t *testing.T) { log := utils.NewNopZapLogger() @@ -471,10 +475,10 @@ func TestSubscribeNewHeadsHistorical(t *testing.T) { require.NoError(t, err) testDB := pebble.NewMemTest(t) - chain := blockchain.New(testDB, &utils.Mainnet) + chain := blockchain.New(testDB, &utils.Mainnet, nil) assert.NoError(t, chain.Store(block0, &emptyCommitments, stateUpdate0, nil)) - chain = blockchain.New(testDB, &utils.Mainnet) + chain = blockchain.New(testDB, &utils.Mainnet, nil) syncer := newFakeSyncer() ctx, cancel := context.WithCancel(context.Background()) diff --git a/sync/sync.go b/sync/sync.go index c45e1f55bb..ae9617429d 100644 --- a/sync/sync.go +++ b/sync/sync.go @@ -55,6 +55,10 @@ type Reader interface { SubscribeNewHeads() HeaderSubscription SubscribeReorg() ReorgSubscription SubscribePendingTxs() PendingTxSubscription + + Pending() (*Pending, error) + PendingBlock() *core.Block + PendingState() (core.StateReader, func() error, error) } // This is temporary and will be removed once the p2p synchronizer implements this interface. @@ -92,6 +96,18 @@ type ReorgData struct { EndBlockNum uint64 `json:"ending_block_number"` } +func (n *NoopSynchronizer) PendingBlock() *core.Block { + return nil +} + +func (n *NoopSynchronizer) Pending() (*Pending, error) { + return nil, errors.New("Pending() is not implemented") +} + +func (n *NoopSynchronizer) PendingState() (core.StateReader, func() error, error) { + return nil, nil, errors.New("PendingState() not implemented") +} + // Synchronizer manages a list of StarknetData to fetch the latest blockchain updates type Synchronizer struct { blockchain *blockchain.Blockchain @@ -571,3 +587,78 @@ func (s *Synchronizer) SubscribePendingTxs() PendingTxSubscription { Subscription: s.pendingTxsFeed.Subscribe(), } } + +// StorePending stores a pending block given that it is for the next height +func (s *Synchronizer) StorePending(p *Pending) error { + err := blockchain.CheckBlockVersion(p.Block.ProtocolVersion) + if err != nil { + return err + } + + expectedParentHash := new(felt.Felt) + h, err := s.blockchain.HeadsHeader() + if err != nil && !errors.Is(err, db.ErrKeyNotFound) { + return err + } else if err == nil { + expectedParentHash = h.Hash + } + + if !expectedParentHash.Equal(p.Block.ParentHash) { + return fmt.Errorf("store pending: %w", blockchain.ErrParentDoesNotMatchHead) + } + + if existingPending, err := s.Pending(); err == nil { + if existingPending.Block.TransactionCount >= p.Block.TransactionCount { + // ignore the incoming pending if it has fewer transactions than the one we already have + return nil + } + } else if !errors.Is(err, ErrPendingBlockNotFound) { + return err + } + s.pending.Store(p) + + return nil +} + +func (s *Synchronizer) Pending() (*Pending, error) { + p := s.pending.Load() + if p == nil { + return nil, ErrPendingBlockNotFound + } + + expectedParentHash := &felt.Zero + if head, err := s.blockchain.HeadsHeader(); err == nil { + expectedParentHash = head.Hash + } + if p.Block.ParentHash.Equal(expectedParentHash) { + return p, nil + } + + // Since the pending block in the cache is outdated remove it + s.pending.Store(nil) + + return nil, ErrPendingBlockNotFound +} + +func (s *Synchronizer) PendingBlock() *core.Block { + pending, err := s.Pending() + if err != nil { + return nil + } + return pending.Block +} + +// PendingState returns the state resulting from execution of the pending block +func (s *Synchronizer) PendingState() (core.StateReader, func() error, error) { + txn, err := s.db.NewTransaction(false) + if err != nil { + return nil, nil, err + } + + pending, err := s.Pending() + if err != nil { + return nil, nil, utils.RunAndWrapOnError(txn.Discard, err) + } + + return NewPendingState(pending.StateUpdate.StateDiff, pending.NewClasses, core.NewState(txn)), txn.Discard, nil +} diff --git a/sync/sync_test.go b/sync/sync_test.go index 3839154322..38ec896c4a 100644 --- a/sync/sync_test.go +++ b/sync/sync_test.go @@ -164,7 +164,7 @@ func TestReorg(t *testing.T) { integStart, err := bc.BlockHeaderByNumber(0) require.NoError(t, err) - synchronizer = sync.New(bc, mainGw, utils.NewNopZapLogger(), 0, false) + synchronizer = sync.New(bc, mainGw, utils.NewNopZapLogger(), 0, false, testDB) sub := synchronizer.SubscribeReorg() ctx, cancel = context.WithTimeout(context.Background(), timeout) require.NoError(t, synchronizer.Run(ctx)) @@ -185,6 +185,28 @@ func TestReorg(t *testing.T) { }) } +func TestPending(t *testing.T) { + t.Parallel() + + client := feeder.NewTestClient(t, &utils.Mainnet) + gw := adaptfeeder.New(client) + + testDB := pebble.NewMemTest(t) + log := utils.NewNopZapLogger() + bc := blockchain.New(testDB, &utils.Mainnet, nil) + synchronizer := sync.New(bc, gw, log, time.Millisecond*100, false, testDB) + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + + require.NoError(t, synchronizer.Run(ctx)) + cancel() + + head, err := bc.HeadsHeader() + require.NoError(t, err) + pending, err := synchronizer.Pending() + require.NoError(t, err) + assert.Equal(t, head.Hash, pending.Block.ParentHash) +} + func TestSubscribeNewHeads(t *testing.T) { t.Parallel() testDB := pebble.NewMemTest(t) @@ -217,8 +239,8 @@ func TestSubscribePendingTxs(t *testing.T) { testDB := pebble.NewMemTest(t) log := utils.NewNopZapLogger() - bc := blockchain.New(testDB, &utils.Mainnet) - synchronizer := sync.New(bc, gw, log, time.Millisecond*100, false) + bc := blockchain.New(testDB, &utils.Mainnet, nil) + synchronizer := sync.New(bc, gw, log, time.Millisecond*100, false, testDB) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) sub := synchronizer.SubscribePendingTxs() @@ -226,7 +248,7 @@ func TestSubscribePendingTxs(t *testing.T) { require.NoError(t, synchronizer.Run(ctx)) cancel() - pending, err := bc.Pending() + pending, err := synchronizer.Pending() require.NoError(t, err) pendingTxs, ok := <-sub.Recv() require.True(t, ok) From 9c99505a251574c6f48bf4f263ee063231e796a9 Mon Sep 17 00:00:00 2001 From: weiihann Date: Fri, 13 Dec 2024 10:11:18 +0800 Subject: [PATCH 26/26] lint sub events --- rpc/handlers.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/rpc/handlers.go b/rpc/handlers.go index fc541b17b8..80ec88279f 100644 --- a/rpc/handlers.go +++ b/rpc/handlers.go @@ -66,12 +66,10 @@ var ( ErrUnsupportedTxVersion = &jsonrpc.Error{Code: 61, Message: "the transaction version is not supported"} ErrUnsupportedContractClassVersion = &jsonrpc.Error{Code: 62, Message: "the contract class version is not supported"} ErrUnexpectedError = &jsonrpc.Error{Code: 63, Message: "An unexpected error occurred"} + ErrTooManyAddressesInFilter = &jsonrpc.Error{Code: 67, Message: "Too many addresses in filter sender_address filter"} ErrTooManyBlocksBack = &jsonrpc.Error{Code: 68, Message: fmt.Sprintf("Cannot go back more than %v blocks", maxBlocksBack)} ErrCallOnPending = &jsonrpc.Error{Code: 69, Message: "This method does not support being called on the pending block"} - ErrTooManyAddressesInFilter = &jsonrpc.Error{Code: 67, Message: "Too many addresses in filter sender_address filter"} - ErrTooManyBlocksBack = &jsonrpc.Error{Code: 68, Message: "Cannot go back more than 1024 blocks"} - // These errors can be only be returned by Juno-specific methods. ErrSubscriptionNotFound = &jsonrpc.Error{Code: 100, Message: "Subscription not found"} ) @@ -352,6 +350,11 @@ func (h *Handler) Methods() ([]jsonrpc.Method, string) { //nolint: funlen Name: "starknet_specVersion", Handler: h.SpecVersion, }, + { + Name: "starknet_subscribeEvents", + Params: []jsonrpc.Parameter{{Name: "from_address", Optional: true}, {Name: "keys", Optional: true}, {Name: "block", Optional: true}}, + Handler: h.SubscribeEvents, + }, { Name: "starknet_subscribeNewHeads", Params: []jsonrpc.Parameter{{Name: "block", Optional: true}},