From 4e93eb2b11db758431e3b3fb6b1bd17c6695619f Mon Sep 17 00:00:00 2001 From: weiihann Date: Thu, 10 Oct 2024 15:46:07 +0800 Subject: [PATCH 01/31] 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 6ed46272007adb391f9b935cfbef5622c0e8a315 Mon Sep 17 00:00:00 2001 From: weiihann Date: Mon, 14 Oct 2024 14:42:11 +0800 Subject: [PATCH 02/31] 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 f58a1a1a21898ee3b1d48720052d026826fcb1cc Mon Sep 17 00:00:00 2001 From: weiihann Date: Mon, 14 Oct 2024 17:25:49 +0800 Subject: [PATCH 03/31] 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 c9b73a052dca460573b8a6b9a1dfe0f066d3288f Mon Sep 17 00:00:00 2001 From: weiihann Date: Mon, 14 Oct 2024 18:36:52 +0800 Subject: [PATCH 04/31] 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 79d29e3175ae19d9905fff051320979630c085dc Mon Sep 17 00:00:00 2001 From: weiihann Date: Mon, 14 Oct 2024 18:41:55 +0800 Subject: [PATCH 05/31] 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 0c566db9548484a9ec86883c7f678247cda3a805 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 15 Oct 2024 17:39:43 +0800 Subject: [PATCH 06/31] 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 036f8d9b8380073496f282d1fd3a29514d402648 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 22 Oct 2024 00:19:20 +0800 Subject: [PATCH 07/31] 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 227c9a930199fa813253c7e5052ec76ce1d7a0b4 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 22 Oct 2024 00:50:20 +0800 Subject: [PATCH 08/31] 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 0aca3a5586965bdd4176c64dabb3f30c4b30b347 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 22 Oct 2024 00:56:57 +0800 Subject: [PATCH 09/31] 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 807756063e241a9fdedf294919b66c302e6ac55d Mon Sep 17 00:00:00 2001 From: weiihann Date: Fri, 25 Oct 2024 19:03:32 +0800 Subject: [PATCH 10/31] put IsNil in utils package --- jsonrpc/server.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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) From 446b612fa78d98c3acd6afbac1ec8b48f1867731 Mon Sep 17 00:00:00 2001 From: weiihann Date: Fri, 25 Oct 2024 19:56:35 +0800 Subject: [PATCH 11/31] 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 520fc394ef91e1838c6feac064cfd51267305405 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 29 Oct 2024 17:29:47 +0800 Subject: [PATCH 12/31] 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 74604766164c4d6ea4434f16e38db6a35228ce51 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 10:04:11 +0800 Subject: [PATCH 13/31] 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 664581efdc575ce93a1f457537607623ca07d4fb Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 10:13:32 +0800 Subject: [PATCH 14/31] 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 d0ff6973d36e20d3f8f96c07f4d4540f571a9ac8 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 10:22:14 +0800 Subject: [PATCH 15/31] 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 92845eb96edf796332e703ab222c0995236e82df Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 12:05:57 +0800 Subject: [PATCH 16/31] 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 bcec5ad7e79dce4050cb7bbc10280d980c32d257 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 12:18:19 +0800 Subject: [PATCH 17/31] 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 dbb124d6acc869d0ad69ad281593475718211d42 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 12:28:44 +0800 Subject: [PATCH 18/31] 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 71f343823eae267501ba01ad7bac01d4051067aa Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 13:00:26 +0800 Subject: [PATCH 19/31] 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 5f754bfa107e489e9713efcc45aa9333922804a9 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 13:15:08 +0800 Subject: [PATCH 20/31] 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 e94f1782bc78714a4b37f706ccb3f5d2cfe1113d Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 13:26:31 +0800 Subject: [PATCH 21/31] 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 de679b60de993f3131b857182e4b37b0aabda150 Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 13:28:39 +0800 Subject: [PATCH 22/31] 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 2cbdf74a9190f0e10567c588168eebedcab5a49c Mon Sep 17 00:00:00 2001 From: weiihann Date: Tue, 10 Dec 2024 13:34:04 +0800 Subject: [PATCH 23/31] 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 afb16823e88a23b71aad699ec0427a36b6482d85 Mon Sep 17 00:00:00 2001 From: weiihann Date: Thu, 12 Dec 2024 23:36:18 +0800 Subject: [PATCH 24/31] 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 da2a122374..a29e795559 100644 --- a/cmd/juno/dbcmd.go +++ b/cmd/juno/dbcmd.go @@ -86,7 +86,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 32695222645e6c5b3867eb05df087120a8c96192 Mon Sep 17 00:00:00 2001 From: weiihann Date: Fri, 13 Dec 2024 10:05:56 +0800 Subject: [PATCH 25/31] 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 a29e795559..da2a122374 100644 --- a/cmd/juno/dbcmd.go +++ b/cmd/juno/dbcmd.go @@ -86,7 +86,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 32ff715fcdb564e6881a665d58983b1c7ec3c808 Mon Sep 17 00:00:00 2001 From: weiihann Date: Fri, 13 Dec 2024 10:11:18 +0800 Subject: [PATCH 26/31] 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}}, From 45c0dd60e27a91032472e4ea49436d8bf6003a9f Mon Sep 17 00:00:00 2001 From: weiihann Date: Wed, 18 Dec 2024 19:03:15 +0800 Subject: [PATCH 27/31] create ReorgEvent --- rpc/handlers.go | 4 ++-- rpc/subscriptions.go | 23 +++++++++++++++++------ rpc/subscriptions_test.go | 6 +++--- sync/sync.go | 16 ++++++++-------- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/rpc/handlers.go b/rpc/handlers.go index 80ec88279f..7657d021a8 100644 --- a/rpc/handlers.go +++ b/rpc/handlers.go @@ -96,7 +96,7 @@ type Handler struct { version string newHeads *feed.Feed[*core.Header] - reorgs *feed.Feed[*sync.ReorgData] + reorgs *feed.Feed[*sync.ReorgBlockRange] pendingTxs *feed.Feed[[]core.Transaction] idgen func() uint64 @@ -138,7 +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](), + reorgs: feed.New[*sync.ReorgBlockRange](), pendingTxs: feed.New[[]core.Transaction](), subscriptions: make(map[uint64]*subscription), diff --git a/rpc/subscriptions.go b/rpc/subscriptions.go index 5f50500499..0b3fa84eb3 100644 --- a/rpc/subscriptions.go +++ b/rpc/subscriptions.go @@ -284,7 +284,7 @@ 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{} { +func (h *Handler) filterTxs(pendingTxs []core.Transaction, getDetails bool, senderAddr []felt.Felt) any { if getDetails { return h.filterTxDetails(pendingTxs, senderAddr) } @@ -386,8 +386,7 @@ func (h *Handler) resolveBlockRange(blockID *BlockID) (*core.Header, *core.Heade // 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, + startHeader, latestHeader *core.Header, w jsonrpc.Conn, id uint64, ) error { @@ -448,7 +447,7 @@ 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) { +func (h *Handler) processReorgs(ctx context.Context, reorgSub *feed.Subscription[*sync.ReorgBlockRange], w jsonrpc.Conn, id uint64) { for { select { case <-ctx.Done(): @@ -462,13 +461,25 @@ func (h *Handler) processReorgs(ctx context.Context, reorgSub *feed.Subscription } } -func (h *Handler) sendReorg(w jsonrpc.Conn, reorg *sync.ReorgData, id uint64) error { +type ReorgEvent struct { + StartBlockHash *felt.Felt `json:"starting_block_hash"` + StartBlockNum uint64 `json:"starting_block_number"` + EndBlockHash *felt.Felt `json:"ending_block_hash"` + EndBlockNum uint64 `json:"ending_block_number"` +} + +func (h *Handler) sendReorg(w jsonrpc.Conn, reorg *sync.ReorgBlockRange, id uint64) error { resp, err := json.Marshal(jsonrpc.Request{ Version: "2.0", Method: "starknet_subscriptionReorg", Params: map[string]any{ "subscription_id": id, - "result": reorg, + "result": &ReorgEvent{ + StartBlockHash: reorg.StartBlockHash, + StartBlockNum: reorg.StartBlockNum, + EndBlockHash: reorg.EndBlockHash, + EndBlockNum: reorg.EndBlockNum, + }, }, }) if err != nil { diff --git a/rpc/subscriptions_test.go b/rpc/subscriptions_test.go index a58b68bd72..c739709661 100644 --- a/rpc/subscriptions_test.go +++ b/rpc/subscriptions_test.go @@ -332,14 +332,14 @@ func TestSubscribeEvents(t *testing.T) { type fakeSyncer struct { newHeads *feed.Feed[*core.Header] - reorgs *feed.Feed[*sync.ReorgData] + reorgs *feed.Feed[*sync.ReorgBlockRange] pendingTxs *feed.Feed[[]core.Transaction] } func newFakeSyncer() *fakeSyncer { return &fakeSyncer{ newHeads: feed.New[*core.Header](), - reorgs: feed.New[*sync.ReorgData](), + reorgs: feed.New[*sync.ReorgBlockRange](), pendingTxs: feed.New[[]core.Transaction](), } } @@ -623,7 +623,7 @@ func TestSubscriptionReorg(t *testing.T) { } // Simulate a reorg - syncer.reorgs.Send(&sync.ReorgData{ + syncer.reorgs.Send(&sync.ReorgBlockRange{ StartBlockHash: utils.HexToFelt(t, "0x4e1f77f39545afe866ac151ac908bd1a347a2a8a7d58bef1276db4f06fdf2f6"), StartBlockNum: 0, EndBlockHash: utils.HexToFelt(t, "0x34e815552e42c5eb5233b99de2d3d7fd396e575df2719bf98e7ed2794494f86"), diff --git a/sync/sync.go b/sync/sync.go index ae9617429d..cd9fe2c3a0 100644 --- a/sync/sync.go +++ b/sync/sync.go @@ -39,7 +39,7 @@ type HeaderSubscription struct { } type ReorgSubscription struct { - *feed.Subscription[*ReorgData] + *feed.Subscription[*ReorgBlockRange] } type PendingTxSubscription struct { @@ -77,15 +77,15 @@ func (n *NoopSynchronizer) SubscribeNewHeads() HeaderSubscription { } func (n *NoopSynchronizer) SubscribeReorg() ReorgSubscription { - return ReorgSubscription{feed.New[*ReorgData]().Subscribe()} + return ReorgSubscription{feed.New[*ReorgBlockRange]().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 { +// ReorgBlockRange represents data about reorganised blocks, starting and ending block number and hash +type ReorgBlockRange 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 @@ -117,7 +117,7 @@ type Synchronizer struct { startingBlockNumber *uint64 highestBlockHeader atomic.Pointer[core.Header] newHeads *feed.Feed[*core.Header] - reorgFeed *feed.Feed[*ReorgData] + reorgFeed *feed.Feed[*ReorgBlockRange] pendingTxsFeed *feed.Feed[[]core.Transaction] log utils.SimpleLogger @@ -128,7 +128,7 @@ type Synchronizer struct { catchUpMode bool plugin junoplugin.JunoPlugin - currReorg *ReorgData // If nil, no reorg is happening + currReorg *ReorgBlockRange // If nil, no reorg is happening } func New(bc *blockchain.Blockchain, starkNetData starknetdata.StarknetData, log utils.SimpleLogger, @@ -140,7 +140,7 @@ func New(bc *blockchain.Blockchain, starkNetData starknetdata.StarknetData, log starknetData: starkNetData, log: log, newHeads: feed.New[*core.Header](), - reorgFeed: feed.New[*ReorgData](), + reorgFeed: feed.New[*ReorgBlockRange](), pendingTxsFeed: feed.New[[]core.Transaction](), pendingPollInterval: pendingPollInterval, listener: &SelectiveListener{}, @@ -446,7 +446,7 @@ func (s *Synchronizer) revertHead(forkBlock *core.Block) { } if s.currReorg == nil { // first block of the reorg - s.currReorg = &ReorgData{ + s.currReorg = &ReorgBlockRange{ StartBlockHash: localHead, StartBlockNum: head.Number, EndBlockHash: localHead, From 7e13e0bb35e7ec8f36f64b21be1aa8f97259d22a Mon Sep 17 00:00:00 2001 From: weiihann Date: Wed, 18 Dec 2024 20:57:18 +0800 Subject: [PATCH 28/31] remove json tags --- sync/sync.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sync/sync.go b/sync/sync.go index cd9fe2c3a0..0bfdcef9e8 100644 --- a/sync/sync.go +++ b/sync/sync.go @@ -87,13 +87,13 @@ func (n *NoopSynchronizer) SubscribePendingTxs() PendingTxSubscription { // ReorgBlockRange represents data about reorganised blocks, starting and ending block number and hash type ReorgBlockRange struct { // StartBlockHash is the hash of the first known block of the orphaned chain - StartBlockHash *felt.Felt `json:"starting_block_hash"` + StartBlockHash *felt.Felt // StartBlockNum is the number of the first known block of the orphaned chain - StartBlockNum uint64 `json:"starting_block_number"` + StartBlockNum uint64 // The last known block of the orphaned chain - EndBlockHash *felt.Felt `json:"ending_block_hash"` + EndBlockHash *felt.Felt // Number of the last known block of the orphaned chain - EndBlockNum uint64 `json:"ending_block_number"` + EndBlockNum uint64 } func (n *NoopSynchronizer) PendingBlock() *core.Block { From 6e725e9b3cc1eeea70c82bec2db6e8bbdbf32a0e Mon Sep 17 00:00:00 2001 From: weiihann Date: Mon, 23 Dec 2024 17:17:51 +0800 Subject: [PATCH 29/31] nit chore --- rpc/subscriptions.go | 9 +++------ sync/sync.go | 30 +++++++++++++++--------------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/rpc/subscriptions.go b/rpc/subscriptions.go index 0b3fa84eb3..371219f165 100644 --- a/rpc/subscriptions.go +++ b/rpc/subscriptions.go @@ -259,10 +259,7 @@ func (h *Handler) SubscribePendingTxs(ctx context.Context, getDetails *bool, sen return &SubscriptionID{ID: id}, nil } -func (h *Handler) processPendingTxs( - ctx context.Context, - getDetails bool, - senderAddr []felt.Felt, +func (h *Handler) processPendingTxs(ctx context.Context, getDetails bool, senderAddr []felt.Felt, pendingTxsSub *feed.Subscription[[]core.Transaction], w jsonrpc.Conn, id uint64, @@ -338,11 +335,11 @@ func (h *Handler) filterTxBySender(txn core.Transaction, senderAddr []felt.Felt) return false } -func (h *Handler) sendPendingTxs(w jsonrpc.Conn, result interface{}, id uint64) error { +func (h *Handler) sendPendingTxs(w jsonrpc.Conn, result any, id uint64) error { resp, err := json.Marshal(SubscriptionResponse{ Version: "2.0", Method: "starknet_subscriptionPendingTransactions", - Params: map[string]interface{}{ + Params: map[string]any{ "subscription_id": id, "result": result, }, diff --git a/sync/sync.go b/sync/sync.go index 0bfdcef9e8..499bd396ba 100644 --- a/sync/sync.go +++ b/sync/sync.go @@ -46,6 +46,18 @@ type PendingTxSubscription struct { *feed.Subscription[[]core.Transaction] } +// ReorgBlockRange represents data about reorganised blocks, starting and ending block number and hash +type ReorgBlockRange struct { + // StartBlockHash is the hash of the first known block of the orphaned chain + StartBlockHash *felt.Felt + // StartBlockNum is the number of the first known block of the orphaned chain + StartBlockNum uint64 + // The last known block of the orphaned chain + EndBlockHash *felt.Felt + // Number of the last known block of the orphaned chain + EndBlockNum uint64 +} + // 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 @@ -84,18 +96,6 @@ func (n *NoopSynchronizer) SubscribePendingTxs() PendingTxSubscription { return PendingTxSubscription{feed.New[[]core.Transaction]().Subscribe()} } -// ReorgBlockRange represents data about reorganised blocks, starting and ending block number and hash -type ReorgBlockRange struct { - // StartBlockHash is the hash of the first known block of the orphaned chain - StartBlockHash *felt.Felt - // StartBlockNum is the number of the first known block of the orphaned chain - StartBlockNum uint64 - // The last known block of the orphaned chain - EndBlockHash *felt.Felt - // Number of the last known block of the orphaned chain - EndBlockNum uint64 -} - func (n *NoopSynchronizer) PendingBlock() *core.Block { return nil } @@ -548,9 +548,6 @@ 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, @@ -617,6 +614,9 @@ func (s *Synchronizer) StorePending(p *Pending) error { } s.pending.Store(p) + // send the pending transactions to the feed + s.pendingTxsFeed.Send(p.Block.Transactions) + return nil } From 9104a8cc3d06d6a3196796905143763bd1a6d31a Mon Sep 17 00:00:00 2001 From: weiihann Date: Mon, 23 Dec 2024 17:17:59 +0800 Subject: [PATCH 30/31] add empty optional params test --- jsonrpc/server_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/jsonrpc/server_test.go b/jsonrpc/server_test.go index 603db85915..7a41d255d3 100644 --- a/jsonrpc/server_test.go +++ b/jsonrpc/server_test.go @@ -174,6 +174,13 @@ func TestHandle(t *testing.T) { return 0, jsonrpc.Err(jsonrpc.InternalError, nil) }, }, + { + Name: "emptyOptionalParam", + Params: []jsonrpc.Parameter{{Name: "param", Optional: true}}, + Handler: func(param *int) (int, *jsonrpc.Error) { + return 0, nil + }, + }, } listener := CountingEventListener{} @@ -475,6 +482,10 @@ func TestHandle(t *testing.T) { res: `{"jsonrpc":"2.0","error":{"code":-32603,"message":"Internal error"},"id":1}`, checkFailedEvent: true, }, + "empty optional param": { + req: `{"jsonrpc": "2.0", "method": "emptyOptionalParam", "params": {}, "id": 1}`, + res: `{"jsonrpc":"2.0","result":0,"id":1}`, + }, } for desc, test := range tests { From 30e942cfd743650fde93f6fafd8c91b480eb1245 Mon Sep 17 00:00:00 2001 From: weiihann Date: Mon, 23 Dec 2024 22:38:39 +0800 Subject: [PATCH 31/31] add null optional param test --- jsonrpc/server_test.go | 8 +++-- sync/sync_test.go | 79 +++++++++++++++++++++++++++++++++++------- 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/jsonrpc/server_test.go b/jsonrpc/server_test.go index 7a41d255d3..71218a429d 100644 --- a/jsonrpc/server_test.go +++ b/jsonrpc/server_test.go @@ -175,7 +175,7 @@ func TestHandle(t *testing.T) { }, }, { - Name: "emptyOptionalParam", + Name: "singleOptionalParam", Params: []jsonrpc.Parameter{{Name: "param", Optional: true}}, Handler: func(param *int) (int, *jsonrpc.Error) { return 0, nil @@ -483,7 +483,11 @@ func TestHandle(t *testing.T) { checkFailedEvent: true, }, "empty optional param": { - req: `{"jsonrpc": "2.0", "method": "emptyOptionalParam", "params": {}, "id": 1}`, + req: `{"jsonrpc": "2.0", "method": "singleOptionalParam", "params": {}, "id": 1}`, + res: `{"jsonrpc":"2.0","result":0,"id":1}`, + }, + "null optional param": { + req: `{"jsonrpc": "2.0", "method": "singleOptionalParam", "id": 1}`, res: `{"jsonrpc":"2.0","result":0,"id":1}`, }, } diff --git a/sync/sync_test.go b/sync/sync_test.go index 38ec896c4a..2b6d514e88 100644 --- a/sync/sync_test.go +++ b/sync/sync_test.go @@ -186,25 +186,80 @@ func TestReorg(t *testing.T) { } func TestPending(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, 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() + chain := blockchain.New(testDB, &utils.Mainnet, synchronizer.PendingBlock) + synchronizer = sync.New(chain, gw, utils.NewNopZapLogger(), 0, false, testDB) - head, err := bc.HeadsHeader() + b, err := gw.BlockByNumber(context.Background(), 0) require.NoError(t, err) - pending, err := synchronizer.Pending() + su, err := gw.StateUpdate(context.Background(), 0) require.NoError(t, err) - assert.Equal(t, head.Hash, pending.Block.ParentHash) + + 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) + }) } func TestSubscribeNewHeads(t *testing.T) {