github.com/MetalBlockchain/metalgo@v1.11.9/tests/fixture/test_data_server.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package fixture
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"net"
    13  	"net/http"
    14  	"net/url"
    15  	"strconv"
    16  	"strings"
    17  	"sync"
    18  	"time"
    19  
    20  	"github.com/MetalBlockchain/metalgo/utils"
    21  	"github.com/MetalBlockchain/metalgo/utils/crypto/secp256k1"
    22  )
    23  
    24  const (
    25  	allocateKeysPath                  = "/allocateKeys"
    26  	keyCountParameterName             = "count"
    27  	requestedKeyCountExceedsAvailable = "requested key count exceeds available allocation"
    28  )
    29  
    30  var (
    31  	errRequestedKeyCountExceedsAvailable = errors.New(requestedKeyCountExceedsAvailable)
    32  	errInvalidKeyCount                   = errors.New("key count must be greater than zero")
    33  )
    34  
    35  type TestData struct {
    36  	PreFundedKeys []*secp256k1.PrivateKey
    37  }
    38  
    39  // http server allocating resources to tests potentially executing in parallel
    40  type testDataServer struct {
    41  	// Synchronizes access to test data
    42  	lock sync.Mutex
    43  	TestData
    44  }
    45  
    46  // Type used to marshal/unmarshal a set of test keys for transmission over http.
    47  type keysDocument struct {
    48  	Keys []*secp256k1.PrivateKey `json:"keys"`
    49  }
    50  
    51  func (s *testDataServer) allocateKeys(w http.ResponseWriter, r *http.Request) {
    52  	// Attempt to parse the count parameter
    53  	rawKeyCount := r.URL.Query().Get(keyCountParameterName)
    54  	if len(rawKeyCount) == 0 {
    55  		msg := fmt.Sprintf("missing %q parameter", keyCountParameterName)
    56  		http.Error(w, msg, http.StatusBadRequest)
    57  		return
    58  	}
    59  	keyCount, err := strconv.Atoi(rawKeyCount)
    60  	if err != nil {
    61  		msg := fmt.Sprintf("unable to parse %q parameter: %v", keyCountParameterName, err)
    62  		http.Error(w, msg, http.StatusBadRequest)
    63  		return
    64  	}
    65  
    66  	// Ensure a key will be allocated at most once
    67  	s.lock.Lock()
    68  	defer s.lock.Unlock()
    69  
    70  	// Only fulfill requests for available keys
    71  	if keyCount > len(s.PreFundedKeys) {
    72  		http.Error(w, requestedKeyCountExceedsAvailable, http.StatusInternalServerError)
    73  		return
    74  	}
    75  
    76  	// Allocate the requested number of keys
    77  	remainingKeys := len(s.PreFundedKeys) - keyCount
    78  	allocatedKeys := s.PreFundedKeys[remainingKeys:]
    79  
    80  	keysDoc := &keysDocument{
    81  		Keys: allocatedKeys,
    82  	}
    83  	if err := json.NewEncoder(w).Encode(keysDoc); err != nil {
    84  		msg := fmt.Sprintf("failed to encode test keys: %v", err)
    85  		http.Error(w, msg, http.StatusInternalServerError)
    86  		return
    87  	}
    88  
    89  	// Forget the allocated keys
    90  	utils.ZeroSlice(allocatedKeys)
    91  	s.PreFundedKeys = s.PreFundedKeys[:remainingKeys]
    92  }
    93  
    94  // Serve test data via http to ensure allocation is synchronized even when
    95  // ginkgo specs are executing in parallel. Returns the URI to access the server.
    96  func ServeTestData(testData TestData) (string, error) {
    97  	// Listen on a dynamic port to avoid conflicting with other applications
    98  	listener, err := net.Listen("tcp", "127.0.0.1:0")
    99  	if err != nil {
   100  		return "", fmt.Errorf("failed to initialize listener for test data server: %w", err)
   101  	}
   102  	address := fmt.Sprintf("http://%s", listener.Addr())
   103  
   104  	s := &testDataServer{
   105  		TestData: testData,
   106  	}
   107  	mux := http.NewServeMux()
   108  	mux.HandleFunc(allocateKeysPath, s.allocateKeys)
   109  
   110  	httpServer := &http.Server{
   111  		Handler:           mux,
   112  		ReadHeaderTimeout: 3 * time.Second,
   113  	}
   114  
   115  	go func() {
   116  		// Serve always returns a non-nil error and closes l.
   117  		if err := httpServer.Serve(listener); err != http.ErrServerClosed {
   118  			panic(fmt.Sprintf("unexpected error closing test data server: %v", err))
   119  		}
   120  	}()
   121  
   122  	return address, nil
   123  }
   124  
   125  // Retrieve the specified number of pre-funded test keys from the provided URI. A given
   126  // key is allocated at most once during the life of the test data server.
   127  func AllocatePreFundedKeys(baseURI string, count int) ([]*secp256k1.PrivateKey, error) {
   128  	if count <= 0 {
   129  		return nil, errInvalidKeyCount
   130  	}
   131  
   132  	uri, err := url.Parse(baseURI)
   133  	if err != nil {
   134  		return nil, fmt.Errorf("failed to parse uri: %w", err)
   135  	}
   136  	uri.RawQuery = url.Values{
   137  		keyCountParameterName: {strconv.Itoa(count)},
   138  	}.Encode()
   139  	uri.Path = allocateKeysPath
   140  	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, uri.String(), nil)
   141  	if err != nil {
   142  		return nil, fmt.Errorf("failed to construct request: %w", err)
   143  	}
   144  
   145  	resp, err := http.DefaultClient.Do(req)
   146  	if err != nil {
   147  		return nil, fmt.Errorf("failed to request pre-funded keys: %w", err)
   148  	}
   149  	defer resp.Body.Close()
   150  
   151  	body, err := io.ReadAll(resp.Body)
   152  	if err != nil {
   153  		return nil, fmt.Errorf("failed to read response for pre-funded keys: %w", err)
   154  	}
   155  	if resp.StatusCode != http.StatusOK {
   156  		if strings.TrimSpace(string(body)) == requestedKeyCountExceedsAvailable {
   157  			return nil, errRequestedKeyCountExceedsAvailable
   158  		}
   159  		return nil, fmt.Errorf("test data server returned unexpected status code %d: %v", resp.StatusCode, body)
   160  	}
   161  
   162  	keysDoc := &keysDocument{}
   163  	if err := json.Unmarshal(body, keysDoc); err != nil {
   164  		return nil, fmt.Errorf("failed to unmarshal pre-funded keys: %w", err)
   165  	}
   166  	return keysDoc.Keys, nil
   167  }