go-ethereum/consensus/bor/heimdall/client_test.go
PeaStew dac1a42fa8
Update client.go to allow the use of paths with the heimdall url (#1126)
* Update client.go to allow the use of paths with the heimdall url

As reported to the polygon team 2 months ago in the Ankr polygon slack, the current code prevents the use of a path with the heimdall url, this was fixed in erigon ledgerwatch/erigon@a3a6170 in response to my request by Mark Holt, and it seems this code was just copied from bor where it is incorrect too. This patch fixes that.

* Add reference to path library

* Fix reversed order of values in unit tests and test removal of setting u.path to rawPath (otherwise why do we need rawPath at all) seems like there were work arounds done here in bioth the unit tests AND the makeURL but logic needs to be examined
2024-01-15 09:46:15 +01:00

419 lines
13 KiB
Go

package heimdall
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/big"
"net"
"net/http"
"sync"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/network"
"github.com/ethereum/go-ethereum/consensus/bor/heimdall/checkpoint"
"github.com/ethereum/go-ethereum/consensus/bor/heimdall/milestone"
"github.com/stretchr/testify/require"
)
// HttpHandlerFake defines the handler functions required to serve
// requests to the mock heimdal server for specific functions. Add more handlers
// according to requirements.
type HttpHandlerFake struct {
handleFetchCheckpoint http.HandlerFunc
handleFetchMilestone http.HandlerFunc
handleFetchNoAckMilestone http.HandlerFunc
handleFetchLastNoAckMilestone http.HandlerFunc
}
func (h *HttpHandlerFake) GetCheckpointHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
h.handleFetchCheckpoint.ServeHTTP(w, r)
}
}
func (h *HttpHandlerFake) GetMilestoneHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
h.handleFetchMilestone.ServeHTTP(w, r)
}
}
func (h *HttpHandlerFake) GetNoAckMilestoneHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
h.handleFetchNoAckMilestone.ServeHTTP(w, r)
}
}
func (h *HttpHandlerFake) GetLastNoAckMilestoneHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
h.handleFetchLastNoAckMilestone.ServeHTTP(w, r)
}
}
func CreateMockHeimdallServer(wg *sync.WaitGroup, port int, listener net.Listener, handler *HttpHandlerFake) (*http.Server, error) {
// Create a new server mux
mux := http.NewServeMux()
// Create a route for fetching latest checkpoint
mux.HandleFunc("/checkpoints/latest", func(w http.ResponseWriter, r *http.Request) {
handler.GetCheckpointHandler()(w, r)
})
// Create a route for fetching milestone
mux.HandleFunc("/milestone/latest", func(w http.ResponseWriter, r *http.Request) {
handler.GetMilestoneHandler()(w, r)
})
// Create a route for fetching milestone
mux.HandleFunc("/milestone/noAck/{id}", func(w http.ResponseWriter, r *http.Request) {
handler.GetNoAckMilestoneHandler()(w, r)
})
// Create a route for fetching milestone
mux.HandleFunc("/milestone/lastNoAck", func(w http.ResponseWriter, r *http.Request) {
handler.GetLastNoAckMilestoneHandler()(w, r)
})
// Add other routes as per requirement
// Create the server with given port and mux
srv := &http.Server{
Addr: fmt.Sprintf("localhost:%d", port),
Handler: mux,
}
// Close the listener using the port and immediately consume it below
err := listener.Close()
if err != nil {
return nil, err
}
go func() {
defer wg.Done()
// always returns error. ErrServerClosed on graceful close
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
fmt.Printf("error in server.ListenAndServe(): %v", err)
}
}()
return srv, nil
}
// TestFetchCheckpointFromMockHeimdall tests the heimdall client side logic
// to fetch checkpoints (latest for the scope of test) from a mock heimdall server.
// It can be used for debugging purpose (like response fields, marshalling/unmarshalling, etc).
func TestFetchCheckpointFromMockHeimdall(t *testing.T) {
t.Parallel()
// Create a wait group for sending across the mock server
wg := &sync.WaitGroup{}
wg.Add(1)
// Initialize the fake handler and add a fake checkpoint handler function
handler := &HttpHandlerFake{}
handler.handleFetchCheckpoint = func(w http.ResponseWriter, _ *http.Request) {
err := json.NewEncoder(w).Encode(checkpoint.CheckpointResponse{
Height: "0",
Result: checkpoint.Checkpoint{
Proposer: common.Address{},
StartBlock: big.NewInt(0),
EndBlock: big.NewInt(512),
RootHash: common.Hash{},
BorChainID: "15001",
Timestamp: 0,
},
})
if err != nil {
w.WriteHeader(500) // Return 500 Internal Server Error.
}
}
// Fetch available port
port, listener, err := network.FindAvailablePort()
require.NoError(t, err, "expect no error in finding available port")
// Create mock heimdall server and pass handler instance for setting up the routes
srv, err := CreateMockHeimdallServer(wg, port, listener, handler)
require.NoError(t, err, "expect no error in starting mock heimdall server")
// Create a new heimdall client and use same port for connection
client := NewHeimdallClient(fmt.Sprintf("http://localhost:%d", port))
_, err = client.FetchCheckpoint(context.Background(), -1)
require.NoError(t, err, "expect no error in fetching checkpoint")
// Shutdown the server
err = srv.Shutdown(context.TODO())
require.NoError(t, err, "expect no error in shutting down mock heimdall server")
// Wait for `wg.Done()` to be called in the mock server's routine.
wg.Wait()
}
// TestFetchMilestoneFromMockHeimdall tests the heimdall client side logic
// to fetch milestone from a mock heimdall server.
// It can be used for debugging purpose (like response fields, marshalling/unmarshalling, etc).
func TestFetchMilestoneFromMockHeimdall(t *testing.T) {
t.Parallel()
// Create a wait group for sending across the mock server
wg := &sync.WaitGroup{}
wg.Add(1)
// Initialize the fake handler and add a fake milestone handler function
handler := &HttpHandlerFake{}
handler.handleFetchMilestone = func(w http.ResponseWriter, _ *http.Request) {
err := json.NewEncoder(w).Encode(milestone.MilestoneResponse{
Height: "0",
Result: milestone.Milestone{
Proposer: common.Address{},
StartBlock: big.NewInt(0),
EndBlock: big.NewInt(512),
Hash: common.Hash{},
BorChainID: "15001",
Timestamp: 0,
},
})
if err != nil {
w.WriteHeader(500) // Return 500 Internal Server Error.
}
}
// Fetch available port
port, listener, err := network.FindAvailablePort()
require.NoError(t, err, "expect no error in finding available port")
// Create mock heimdall server and pass handler instance for setting up the routes
srv, err := CreateMockHeimdallServer(wg, port, listener, handler)
require.NoError(t, err, "expect no error in starting mock heimdall server")
// Create a new heimdall client and use same port for connection
client := NewHeimdallClient(fmt.Sprintf("http://localhost:%d", port))
_, err = client.FetchMilestone(context.Background())
require.NoError(t, err, "expect no error in fetching milestone")
// Shutdown the server
err = srv.Shutdown(context.TODO())
require.NoError(t, err, "expect no error in shutting down mock heimdall server")
// Wait for `wg.Done()` to be called in the mock server's routine.
wg.Wait()
}
// TestFetchShutdown tests the heimdall client side logic for context timeout and
// interrupt handling while fetching data from a mock heimdall server.
func TestFetchShutdown(t *testing.T) {
t.Parallel()
// Create a wait group for sending across the mock server
wg := &sync.WaitGroup{}
wg.Add(1)
// Initialize the fake handler and add a fake checkpoint handler function
handler := &HttpHandlerFake{}
// Case1 - Testing context timeout: Create delay in serving requests for simulating timeout. Add delay slightly
// greater than `retryDelay`. This should cause the request to timeout and trigger shutdown
// due to `ctx.Done()`. Expect context timeout error.
handler.handleFetchCheckpoint = func(w http.ResponseWriter, _ *http.Request) {
time.Sleep(100 * time.Millisecond)
err := json.NewEncoder(w).Encode(checkpoint.CheckpointResponse{
Height: "0",
Result: checkpoint.Checkpoint{
Proposer: common.Address{},
StartBlock: big.NewInt(0),
EndBlock: big.NewInt(512),
RootHash: common.Hash{},
BorChainID: "15001",
Timestamp: 0,
},
})
if err != nil {
w.WriteHeader(500) // Return 500 Internal Server Error.
}
}
// Fetch available port
port, listener, err := network.FindAvailablePort()
require.NoError(t, err, "expect no error in finding available port")
// Create mock heimdall server and pass handler instance for setting up the routes
srv, err := CreateMockHeimdallServer(wg, port, listener, handler)
require.NoError(t, err, "expect no error in starting mock heimdall server")
// Create a new heimdall client and use same port for connection
client := NewHeimdallClient(fmt.Sprintf("http://localhost:%d", port))
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
// Expect this to fail due to timeout
_, err = client.FetchCheckpoint(ctx, -1)
require.Equal(t, "context deadline exceeded", err.Error(), "expect the function error to be a context deadline exeeded error")
require.Equal(t, "context deadline exceeded", ctx.Err().Error(), "expect the ctx error to be a context deadline exeeded error")
cancel()
// Case2 - Testing context cancellation. Pass a context with timeout to the request and
// cancel it before timeout. This should cause the request to timeout and trigger shutdown
// due to `ctx.Done()`. Expect context cancellation error.
handler.handleFetchCheckpoint = func(w http.ResponseWriter, _ *http.Request) {
time.Sleep(10 * time.Millisecond)
w.WriteHeader(500) // Return 500 Internal Server Error.
}
ctx, cancel = context.WithTimeout(context.Background(), 50*time.Millisecond) // Use some high value for timeout
// Cancel the context after a delay until we make request
go func(cancel context.CancelFunc) {
time.Sleep(10 * time.Millisecond)
cancel()
}(cancel)
// Expect this to fail due to cancellation
_, err = client.FetchCheckpoint(ctx, -1)
require.Equal(t, "context canceled", err.Error(), "expect the function error to be a context cancelled error")
require.Equal(t, "context canceled", ctx.Err().Error(), "expect the ctx error to be a context cancelled error")
// Case3 - Testing interrupt: Closing the `closeCh` in heimdall client simulating interrupt. This
// should cause the request to fail and throw an error due to `<-closeCh` in fetchWithRetry.
// Expect shutdown detected error.
handler.handleFetchCheckpoint = func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(500) // Return 500 Internal Server Error.
}
// Close the channel after a delay until we make request
go func() {
time.Sleep(1 * time.Second)
close(client.closeCh)
}()
// Expect this to fail due to shutdown
_, err = client.FetchCheckpoint(context.Background(), -1)
require.Equal(t, ErrShutdownDetected.Error(), err.Error(), "expect the function error to be a shutdown detected error")
// Shutdown the server
err = srv.Shutdown(context.TODO())
require.NoError(t, err, "expect no error in shutting down mock heimdall server")
// Wait for `wg.Done()` to be called in the mock server's routine.
wg.Wait()
}
// TestContext includes bunch of simple tests to verify the working of timeout
// based context and cancellation.
func TestContext(t *testing.T) {
t.Parallel()
ctx, cancel1 := context.WithTimeout(context.Background(), 1*time.Second)
// Case1: Done is not yet closed, so Err returns nil.
require.NoError(t, ctx.Err(), "expect nil error")
wg := &sync.WaitGroup{}
// Case2: Check if timeout is being handled
wg.Add(1)
go func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
select {
case <-ctx.Done():
// Expect context deadline exceeded error
require.Equal(t, "context deadline exceeded", ctx.Err().Error(), "expect the ctx error to be a context deadline exceeded error")
case <-time.After(2 * time.Second):
// Case for safely exiting the tests
return
}
}(ctx, wg)
// Case3: Check normal case
ctx, cancel2 := context.WithTimeout(context.Background(), 3*time.Second)
wg.Add(1)
errCh := make(chan error, 1)
go func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
select {
case <-ctx.Done():
// Expect this to never occur, throw explicit error
errCh <- errors.New("unexpectecd call to `ctx.Done()`")
case <-time.After(2 * time.Second):
// Case for safely exiting the tests
errCh <- nil
return
}
}(ctx, wg)
if err := <-errCh; err != nil {
t.Fatalf("err: %v", err)
}
// Case4: Check if cancellation is being handled
ctx, cancel3 := context.WithTimeout(context.Background(), 1*time.Second)
wg.Add(1)
go func(cancel context.CancelFunc) {
time.Sleep(500 * time.Millisecond)
cancel()
}(cancel3)
go func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
select {
case <-ctx.Done():
// Expect context canceled error
require.Equal(t, "context canceled", ctx.Err().Error(), "expect the ctx error to be a context canceled error")
case <-time.After(2 * time.Second):
// Case for safely exiting the tests
return
}
}(ctx, wg)
// Wait for all tests to pass
wg.Wait()
// Cancel all remaining contexts
cancel1()
cancel2()
}
func TestSpanURL(t *testing.T) {
t.Parallel()
url, err := spanURL("http://bor0", 1)
if err != nil {
t.Fatal("got an error", err)
}
const expected = "http://bor0/bor/span/1"
if url.String() != expected {
t.Fatalf("expected URL %q, got %q", expected, url.String())
}
}
func TestStateSyncURL(t *testing.T) {
t.Parallel()
url, err := stateSyncURL("http://bor0", 10, 100)
if err != nil {
t.Fatal("got an error", err)
}
const expected = "http://bor0/clerk/event-record/list?from-id=10&to-time=100&limit=50"
if url.String() != expected {
t.Fatalf("expected URL %q, got %q", expected, url.String())
}
}