diff --git a/config/checkpoints/checkpoints.go b/config/checkpoints/checkpoints.go index 1ac7450..6ac2003 100644 --- a/config/checkpoints/checkpoints.go +++ b/config/checkpoints/checkpoints.go @@ -8,7 +8,9 @@ import ( "strconv" "strings" "sync" - + "context" + "time" + "log" "github.com/BlocSoc-iitr/selene/config" "github.com/avast/retry-go" "gopkg.in/yaml.v2" @@ -107,23 +109,26 @@ func Get(url string) (*http.Response, error) { // @param data: []byte - data to deserialize // @return *uint64, error // Deserializes the given data to uint64 -func DeserializeSlot(data []byte) (*uint64, error) { - var s string - if err := json.Unmarshal(data, &s); err != nil { +func DeserializeSlot(input []byte) (*uint64, error) { + var value interface{} + if err := json.Unmarshal(input, &value); err != nil { return nil, err } - if len(s) > 1 && s[0] == '"' && s[len(s)-1] == '"' { - s = s[1 : len(s)-1] - } - - value, err := strconv.ParseUint(s, 10, 64) - if err != nil { - return nil, err + switch v := value.(type) { + case string: + // Try to parse a string as a number + num, err := strconv.ParseUint(v, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid string format: %s", v) + } + return &num, nil + case float64: + num := uint64(v) + return &num, nil + default: + return nil, fmt.Errorf("unexpected type: %T", v) } - - return &value, nil - } // create a new CheckpointFallback object @@ -170,27 +175,48 @@ func (ch CheckpointFallback) Build() (CheckpointFallback, error) { return ch, fmt.Errorf("no services found for network %s", network) } - serviceList := serviceListRaw.([]interface{}) - for _, service := range serviceList { - serviceMap := service.(map[interface{}]interface{}) - endpoint := serviceMap["endpoint"].(string) - name := serviceMap["name"].(string) - state := serviceMap["state"].(bool) - verification := serviceMap["verification"].(bool) - contacts := serviceMap["contacts"].(*yaml.MapSlice) - notes := serviceMap["notes"].(*yaml.MapSlice) - health := serviceMap["health"].(map[interface{}]interface{}) - healthResult := health["result"].(bool) - healthDate := health["date"].(string) + serviceList, ok := serviceListRaw.([]interface{}) + if !ok { + return ch, fmt.Errorf("expected a list for services in network %s", network) + } + + for _, serviceRaw := range serviceList { + serviceMap, ok := serviceRaw.(map[interface{}]interface{}) + if !ok { + return ch, fmt.Errorf("expected a map for service in network %s", network) + } + + endpoint, _ := serviceMap["endpoint"].(string) // Handle potential nil + name, _ := serviceMap["name"].(string) // Handle potential nil + state, _ := serviceMap["state"].(bool) // Handle potential nil + verification, _ := serviceMap["verification"].(bool) // Handle potential nil + + // Check contacts and notes + var contacts *yaml.MapSlice + if c, ok := serviceMap["contacts"].(*yaml.MapSlice); ok { + contacts = c + } + + var notes *yaml.MapSlice + if n, ok := serviceMap["notes"].(*yaml.MapSlice); ok { + notes = n + } + + healthRaw, ok := serviceMap["health"].(map[interface{}]interface{}) + if !ok { + return ch, fmt.Errorf("expected a map for health in service %s", name) + } + healthResult, _ := healthRaw["result"].(bool) // Handle potential nil + healthDate, _ := healthRaw["date"].(string) // Handle potential nil ch.Services[network] = append(ch.Services[network], CheckpointFallbackService{ - Endpoint: endpoint, - Name: name, - State: state, - Verification: verification, - Contacts: contacts, - Notes: notes, - Health_from_fallback: &Health{ + Endpoint: endpoint, + Name: name, + State: state, + Verification: verification, + Contacts: contacts, + Notes: notes, + Health_from_fallback: &Health{ Result: healthResult, Date: healthDate, }, @@ -201,6 +227,7 @@ func (ch CheckpointFallback) Build() (CheckpointFallback, error) { return ch, nil } + // fetch the latest checkpoint from the given network func (ch CheckpointFallback) FetchLatestCheckpoint(network config.Network) byte256 { services := ch.GetHealthyFallbackServices(network) @@ -231,95 +258,73 @@ func (ch CheckpointFallback) QueryService(endpoint string) (*RawSlotResponse, er // fetch the latest checkpoint from the given services func (ch CheckpointFallback) FetchLatestCheckpointFromServices(services []CheckpointFallbackService) (byte256, error) { - var ( - slots []Slot - wg sync.WaitGroup - slotChan = make(chan Slot) - errorsChan = make(chan error) - ) - - for _, service := range services { - wg.Add(1) - go func(service CheckpointFallbackService) { - defer wg.Done() - raw, err := ch.QueryService(service.Endpoint) - if err != nil { - errorsChan <- fmt.Errorf("failed to fetch checkpoint from service %s: %w", service.Endpoint, err) - return - } - - if len(raw.Data.Slots) > 0 { - for _, slot := range raw.Data.Slots { - if slot.Block_root != nil { - slotChan <- slot - return - } - } - } - }(service) - } - - wg.Wait() - close(slotChan) - close(errorsChan) - - var allErrors error - for err := range errorsChan { - if allErrors == nil { - allErrors = err - } else { - allErrors = fmt.Errorf("%v; %v", allErrors, err) - } - } - - if allErrors != nil { - return byte256{}, allErrors - } - - for slot := range slotChan { - slots = append(slots, slot) - } - - if len(slots) == 0 { - return byte256{}, fmt.Errorf("failed to find max epoch from checkpoint slots") - } - - maxEpochSlot := slots[0] - for _, slot := range slots { - if slot.Epoch > maxEpochSlot.Epoch { - maxEpochSlot = slot - } - } - maxEpoch := maxEpochSlot.Epoch - - var maxEpochSlots []Slot - for _, slot := range slots { - if slot.Epoch == maxEpoch { - maxEpochSlots = append(maxEpochSlots, slot) - } - } - - checkpoints := make(map[byte256]int) - for _, slot := range maxEpochSlots { - if slot.Block_root != nil { - checkpoints[*slot.Block_root]++ - } - } + var ( + slots []Slot + wg sync.WaitGroup + slotChan = make(chan Slot, len(services)) // Buffered channel + errorsChan = make(chan error, len(services)) + ) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + for _, service := range services { + wg.Add(1) + go func(service CheckpointFallbackService) { + defer wg.Done() + raw, err := ch.QueryService(service.Endpoint) + if err != nil { + errorsChan <- fmt.Errorf("failed to fetch checkpoint from service %s: %w", service.Endpoint, err) + return + } + if len(raw.Data.Slots) > 0 { + slotChan <- raw.Data.Slots[0] // Send the first valid slot + } + }(service) + } + + go func() { + wg.Wait() + close(slotChan) + close(errorsChan) + }() + + for { + select { + case slot, ok := <-slotChan: + if !ok { + // Channel closed, all slots processed + if len(slots) == 0 { + return byte256{}, fmt.Errorf("failed to find max epoch from checkpoint slots") + } + return processSlots(slots) + } + slots = append(slots, slot) + case err := <-errorsChan: + if err != nil { + log.Printf("Error fetching checkpoint: %v", err) // Log only if the error is not nil. + } + case <-ctx.Done(): + if len(slots) == 0 { + return byte256{}, ctx.Err() + } + return processSlots(slots) + } + } +} - var mostCommon byte256 - maxCount := 0 - for blockRoot, count := range checkpoints { - if count > maxCount { - mostCommon = blockRoot - maxCount = count - } - } +func processSlots(slots []Slot) (byte256, error) { + maxEpochSlot := slots[0] + for _, slot := range slots { + if slot.Epoch > maxEpochSlot.Epoch { + maxEpochSlot = slot + } + } - if maxCount == 0 { - return byte256{}, fmt.Errorf("no checkpoint found") - } + if maxEpochSlot.Block_root == nil { + return byte256{}, fmt.Errorf("no valid block root found") + } - return mostCommon, nil + return *maxEpochSlot.Block_root, nil } func (ch CheckpointFallback) FetchLatestCheckpointFromApi(url string) (byte256, error) { diff --git a/config/checkpoints/checkpoints_test.go b/config/checkpoints/checkpoints_test.go new file mode 100644 index 0000000..4a119cf --- /dev/null +++ b/config/checkpoints/checkpoints_test.go @@ -0,0 +1,320 @@ +package checkpoints + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "io" + "github.com/BlocSoc-iitr/selene/config" +) + +type CustomTransport struct { + DoFunc func(req *http.Request) (*http.Response, error) +} + +func (t *CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return t.DoFunc(req) +} + +func TestCheckpointFallbackNew(t *testing.T) { + cf := CheckpointFallback{}.New() + + if len(cf.Services) != 0 { + t.Errorf("Expected empty Services map, got %v", cf.Services) + } + + expectedNetworks := []config.Network{config.SEPOLIA, config.MAINNET, config.GOERLI} + if !equalNetworks(cf.Networks, expectedNetworks) { + t.Errorf("Expected Networks %v, got %v", expectedNetworks, cf.Networks) + } +} + +func TestDeserializeSlot(t *testing.T) { + tests := []struct { + input []byte + expected uint64 + hasError bool + }{ + {[]byte(`"12345"`), 12345, false}, + {[]byte(`12345`), 12345, false}, + {[]byte(`"abc"`), 0, true}, + {[]byte(`{}`), 0, true}, + } + + for _, test := range tests { + result, err := DeserializeSlot(test.input) + if test.hasError { + if err == nil { + t.Errorf("Expected error for input %s, got nil", string(test.input)) + } + } else { + if err != nil { + t.Errorf("Unexpected error for input %s: %v", string(test.input), err) + } + if result == nil || *result != test.expected { + t.Errorf("Expected %d, got %v for input %s", test.expected, result, string(test.input)) + } + } + } +} + +func TestGetHealthyFallbackEndpoints(t *testing.T) { + cf := CheckpointFallback{ + Services: map[config.Network][]CheckpointFallbackService{ + config.MAINNET: { + {Endpoint: "http://healthy1.com", Health_from_fallback: &Health{Result: true}}, + {Endpoint: "http://unhealthy.com", Health_from_fallback: &Health{Result: false}}, + {Endpoint: "http://healthy2.com", Health_from_fallback: &Health{Result: true}}, + }, + }, + } + + healthyEndpoints := cf.GetHealthyFallbackEndpoints(config.MAINNET) + expected := []string{"http://healthy1.com", "http://healthy2.com"} + + if !equalStringSlices(healthyEndpoints, expected) { + t.Errorf("Expected healthy endpoints %v, got %v", expected, healthyEndpoints) + } +} + +func TestBuild(t *testing.T) { + yamlData := ` +sepolia: + - endpoint: "https://sepolia1.example.com" + name: "Sepolia 1" + state: true + verification: true + health: + result: true +mainnet: + - endpoint: "https://mainnet1.example.com" + name: "Mainnet 1" + state: true + verification: true + health: + result: true +goerli: + - endpoint: "https://goerli1.example.com" + name: "Goerli 1" + state: true + verification: true + health: + result: true +` + + client := &http.Client{ + Transport: &CustomTransport{ + DoFunc: func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(yamlData)), + Header: make(http.Header), + }, nil + }, + }, + } + + cf := CheckpointFallback{}.New() + originalClient := http.DefaultClient + http.DefaultClient = client + defer func() { http.DefaultClient = originalClient }() + + builtCf, err := cf.Build() + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(builtCf.Services[config.SEPOLIA]) != 1 { + t.Errorf("Expected 1 service for Sepolia, got %d", len(builtCf.Services[config.SEPOLIA])) + } + + if len(builtCf.Services[config.MAINNET]) != 1 { + t.Errorf("Expected 1 service for Mainnet, got %d", len(builtCf.Services[config.MAINNET])) + } + + sepoliaService := builtCf.Services[config.SEPOLIA][0] + if sepoliaService.Endpoint != "https://sepolia1.example.com" { + t.Errorf("Expected endpoint https://sepolia1.example.com, got %s", sepoliaService.Endpoint) + } +} + +func TestFetchLatestCheckpointFromServices(t *testing.T) { + server1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := json.NewEncoder(w).Encode(RawSlotResponse{ + Data: RawSlotResponseData{ + Slots: []Slot{ + {Epoch: 100, Block_root: &byte256{1}}, + }, + }, + }); err != nil { + t.Fatalf("Failed to encode response: %v", err) + } + })) + defer server1.Close() + + server2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := json.NewEncoder(w).Encode(RawSlotResponse{ + Data: RawSlotResponseData{ + Slots: []Slot{ + {Epoch: 101, Block_root: &byte256{2}}, + }, + }, + }); err != nil { + t.Fatalf("Failed to encode response: %v", err) + } + })) + defer server2.Close() + + cf := CheckpointFallback{} + services := []CheckpointFallbackService{ + {Endpoint: server1.URL}, + {Endpoint: server2.URL}, + } + + checkpoint, err := cf.FetchLatestCheckpointFromServices(services) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + expected := byte256{2} + if checkpoint != expected { + t.Errorf("Expected checkpoint %v, got %v", expected, checkpoint) + } +} + +func TestQueryService(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := json.NewEncoder(w).Encode(RawSlotResponse{ + Data: RawSlotResponseData{ + Slots: []Slot{ + {Epoch: 100, Block_root: &byte256{1}}, + }, + }, + }); err != nil { + t.Fatalf("Failed to encode response: %v", err) + } + })) + defer server.Close() + + cf := CheckpointFallback{} + response, err := cf.QueryService(server.URL) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(response.Data.Slots) != 1 { + t.Errorf("Expected 1 slot, got %d", len(response.Data.Slots)) + } + + if response.Data.Slots[0].Epoch != 100 { + t.Errorf("Expected epoch 100, got %d", response.Data.Slots[0].Epoch) + } +} + +func TestFetchLatestCheckpointFromApi(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := json.NewEncoder(w).Encode(RawSlotResponse{ + Data: RawSlotResponseData{ + Slots: []Slot{ + {Epoch: 100, Block_root: &byte256{1}}, + }, + }, + }); err != nil { + t.Fatalf("Failed to encode response: %v", err) + } + })) + defer server.Close() + + cf := CheckpointFallback{} + checkpoint, err := cf.FetchLatestCheckpointFromApi(server.URL) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + expected := byte256{1} + if checkpoint != expected { + t.Errorf("Expected checkpoint %v, got %v", expected, checkpoint) + } +} + +func TestConstructUrl(t *testing.T) { + cf := CheckpointFallback{} + endpoint := "https://example.com" + expected := "https://example.com/checkpointz/v1/beacon/slots" + result := cf.ConstructUrl(endpoint) + if result != expected { + t.Errorf("Expected URL %s, got %s", expected, result) + } +} + +func TestGetAllFallbackEndpoints(t *testing.T) { + cf := CheckpointFallback{ + Services: map[config.Network][]CheckpointFallbackService{ + config.MAINNET: { + {Endpoint: "http://endpoint1.com"}, + {Endpoint: "http://endpoint2.com"}, + }, + }, + } + + endpoints := cf.GetAllFallbackEndpoints(config.MAINNET) + expected := []string{"http://endpoint1.com", "http://endpoint2.com"} + + if !equalStringSlices(endpoints, expected) { + t.Errorf("Expected endpoints %v, got %v", expected, endpoints) + } +} +func TestGetHealthyFallbackServices(t *testing.T) { + cf := CheckpointFallback{ + Services: map[config.Network][]CheckpointFallbackService{ + config.MAINNET: { + {Endpoint: "http://healthy1.com", Health_from_fallback: &Health{Result: true}}, + {Endpoint: "http://unhealthy.com", Health_from_fallback: &Health{Result: false}}, + {Endpoint: "http://healthy2.com", Health_from_fallback: &Health{Result: true}}, + }, + }, + } + + healthyServices := cf.GetHealthyFallbackServices(config.MAINNET) + if len(healthyServices) != 2 { + t.Errorf("Expected 2 healthy services, got %d", len(healthyServices)) + } + + for _, service := range healthyServices { + if !service.Health_from_fallback.Result { + t.Errorf("Expected healthy service, got unhealthy for endpoint %s", service.Endpoint) + } + } +} + + +func equalNetworks(a, b []config.Network) bool { + if len(a) != len(b) { + return false + } + networkMap := make(map[config.Network]bool) + for _, n := range a { + networkMap[n] = true + } + for _, n := range b { + if !networkMap[n] { + return false + } + } + return true +} + +func equalStringSlices(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + diff --git a/config/networks_test.go b/config/networks_test.go new file mode 100644 index 0000000..88fccaf --- /dev/null +++ b/config/networks_test.go @@ -0,0 +1,195 @@ +package config +import ( + "testing" + "github.com/stretchr/testify/assert" + "strings" +) +func TestNetwork_BaseConfig(t *testing.T) { + tests := []struct { + name string + inputNetwork string + expectedChainID uint64 + expectedGenesis uint64 + expectedRPCPort uint16 + checkConsensusRPC func(*testing.T, *string) + wantErr bool + }{ + { + name: "Mainnet", + inputNetwork: "MAINNET", + expectedChainID: 1, + expectedGenesis: 1606824023, + expectedRPCPort: 8545, + checkConsensusRPC: func(t *testing.T, rpc *string) { + assert.NotNil(t, rpc) + assert.Equal(t, "https://www.lightclientdata.org", *rpc) + }, + wantErr: false, + }, + { + name: "Goerli", + inputNetwork: "GOERLI", + expectedChainID: 5, + expectedGenesis: 1616508000, + expectedRPCPort: 8545, + checkConsensusRPC: func(t *testing.T, rpc *string) { + assert.Nil(t, rpc) + }, + wantErr: false, + }, + { + name: "Sepolia", + inputNetwork: "SEPOLIA", + expectedChainID: 11155111, + expectedGenesis: 1655733600, + expectedRPCPort: 8545, + checkConsensusRPC: func(t *testing.T, rpc *string) { + assert.Nil(t, rpc) + }, + wantErr: false, + }, + { + name: "Invalid", + inputNetwork: "INVALID", + expectedChainID: 0, + expectedGenesis: 0, + expectedRPCPort: 0, + checkConsensusRPC: func(t *testing.T, rpc *string) {}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Network("") // The receiver doesn't matter, we're testing the input + config, err := n.BaseConfig(tt.inputNetwork) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedChainID, config.Chain.ChainID) + assert.Equal(t, tt.expectedGenesis, config.Chain.GenesisTime) + assert.Equal(t, tt.expectedRPCPort, config.RpcPort) + tt.checkConsensusRPC(t, config.ConsensusRpc) + + // Check Forks + assert.NotEmpty(t, config.Forks.Genesis) + assert.NotEmpty(t, config.Forks.Altair) + assert.NotEmpty(t, config.Forks.Bellatrix) + assert.NotEmpty(t, config.Forks.Capella) + assert.NotEmpty(t, config.Forks.Deneb) + + // Check MaxCheckpointAge + assert.Equal(t, uint64(1_209_600), config.MaxCheckpointAge) + + // Check DataDir + assert.NotNil(t, config.DataDir) + assert.Contains(t, strings.ToLower(*config.DataDir), strings.ToLower(tt.inputNetwork)) + } + }) + } +} +func TestNetwork_ChainID(t *testing.T) { + tests := []struct { + name string + inputChainID uint64 + expectedChainID uint64 + expectedGenesis uint64 + expectedRPCPort uint16 + checkConsensusRPC func(*testing.T, *string) + wantErr bool + }{ + { + name: "Mainnet", + inputChainID: 1, + expectedChainID: 1, + expectedGenesis: 1606824023, + expectedRPCPort: 8545, + checkConsensusRPC: func(t *testing.T, rpc *string) { + assert.NotNil(t, rpc) + assert.Equal(t, "https://www.lightclientdata.org", *rpc) + }, + wantErr: false, + }, + { + name: "Goerli", + inputChainID: 5, + expectedChainID: 5, + expectedGenesis: 1616508000, + expectedRPCPort: 8545, + checkConsensusRPC: func(t *testing.T, rpc *string) { + assert.Nil(t, rpc) + }, + wantErr: false, + }, + { + name: "Sepolia", + inputChainID: 11155111, + expectedChainID: 11155111, + expectedGenesis: 1655733600, + expectedRPCPort: 8545, + checkConsensusRPC: func(t *testing.T, rpc *string) { + assert.Nil(t, rpc) + }, + wantErr: false, + }, + { + name: "Invalid ChainID", + inputChainID: 9999, // Non-existent ChainID + expectedChainID: 0, + expectedGenesis: 0, + expectedRPCPort: 0, + checkConsensusRPC: func(t *testing.T, rpc *string) {}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Network("") // The receiver doesn't matter, we're testing the input + config, err := n.ChainID(tt.inputChainID) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedChainID, config.Chain.ChainID) + assert.Equal(t, tt.expectedGenesis, config.Chain.GenesisTime) + assert.Equal(t, tt.expectedRPCPort, config.RpcPort) + tt.checkConsensusRPC(t, config.ConsensusRpc) + + // Check Forks + assert.NotEmpty(t, config.Forks.Genesis) + assert.NotEmpty(t, config.Forks.Altair) + assert.NotEmpty(t, config.Forks.Bellatrix) + assert.NotEmpty(t, config.Forks.Capella) + assert.NotEmpty(t, config.Forks.Deneb) + + // Check MaxCheckpointAge + assert.Equal(t, uint64(1_209_600), config.MaxCheckpointAge) + + // Check DataDir + assert.NotNil(t, config.DataDir) + assert.Contains(t, strings.ToLower(*config.DataDir), strings.ToLower(tt.name)) + } + }) + } +} +func TestDataDir(t *testing.T) { + tests := []struct { + name string + network Network + expected string + }{ + {"Mainnet DataDir", MAINNET, "selene/data/mainnet"}, + {"Goerli DataDir", GOERLI, "selene/data/goerli"}, + {"Sepolia DataDir", SEPOLIA, "selene/data/sepolia"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + path, err := dataDir(tt.network) + assert.NoError(t, err) + assert.Contains(t, path, tt.expected) + }) + } +} diff --git a/config/types_test.go b/config/types_test.go new file mode 100644 index 0000000..2c47894 --- /dev/null +++ b/config/types_test.go @@ -0,0 +1,80 @@ +package config + +import ( + "encoding/json" + "testing" + "github.com/BlocSoc-iitr/selene/consensus/consensus_core" +) + +func TestChainConfigMarshalUnmarshal(t *testing.T) { + originalConfig := ChainConfig{ + ChainID: 1, + GenesisTime: 1606824023, + GenesisRoot: []byte{0x4b, 0x36, 0x3d, 0xb9}, + } + + // Marshal ChainConfig to JSON + marshaledData, err := json.Marshal(originalConfig) + if err != nil { + t.Fatalf("Error marshaling ChainConfig: %v", err) + } + + // Unmarshal the JSON back to ChainConfig + var unmarshaledConfig ChainConfig + err = json.Unmarshal(marshaledData, &unmarshaledConfig) + if err != nil { + t.Fatalf("Error unmarshaling ChainConfig: %v", err) + } + + // Verify that the original and unmarshaled configs are the same + if originalConfig.ChainID != unmarshaledConfig.ChainID { + t.Errorf("ChainID mismatch. Got %d, expected %d", unmarshaledConfig.ChainID, originalConfig.ChainID) + } + if originalConfig.GenesisTime != unmarshaledConfig.GenesisTime { + t.Errorf("GenesisTime mismatch. Got %d, expected %d", unmarshaledConfig.GenesisTime, originalConfig.GenesisTime) + } + if string(originalConfig.GenesisRoot) != string(unmarshaledConfig.GenesisRoot) { + t.Errorf("GenesisRoot mismatch. Got %x, expected %x", unmarshaledConfig.GenesisRoot, originalConfig.GenesisRoot) + } +} + +func TestForkMarshalUnmarshal(t *testing.T) { + originalFork := consensus_core.Fork{ + Epoch: 0, + ForkVersion: []byte{0x01, 0x00, 0x00, 0x00}, + } + + // Marshal Fork to JSON + marshaledData, err := json.Marshal(originalFork) + if err != nil { + t.Fatalf("Error marshaling Fork: %v", err) + } + + // Unmarshal the JSON back to Fork + var unmarshaledFork consensus_core.Fork + err = json.Unmarshal(marshaledData, &unmarshaledFork) + if err != nil { + t.Fatalf("Error unmarshaling Fork: %v", err) + } + + // Verify that the original and unmarshaled Fork are the same + if originalFork.Epoch != unmarshaledFork.Epoch { + t.Errorf("Epoch mismatch. Got %d, expected %d", unmarshaledFork.Epoch, originalFork.Epoch) + } + if string(originalFork.ForkVersion) != string(unmarshaledFork.ForkVersion) { + t.Errorf("ForkVersion mismatch. Got %x, expected %x", unmarshaledFork.ForkVersion, originalFork.ForkVersion) + } +} + +func TestUnmarshalInvalidHex(t *testing.T) { + invalidJSON := `{ + "epoch": 0, + "fork_version": "invalid_hex_string" + }` + + var fork consensus_core.Fork + err := json.Unmarshal([]byte(invalidJSON), &fork) + if err == nil { + t.Fatal("Expected error unmarshaling invalid hex string, but got nil") + } +} diff --git a/go.mod b/go.mod index ca4368d..d3224ef 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/consensys/bavard v0.1.13 // indirect github.com/consensys/gnark-crypto v0.12.1 // indirect github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/ethereum/c-kzg-4844 v1.0.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect @@ -24,12 +25,14 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/testify v1.9.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/supranational/blst v0.3.11 // indirect github.com/ugorji/go/codec v1.2.12 // indirect diff --git a/go.sum b/go.sum index 661a12d..602c81c 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDF github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= @@ -40,6 +42,8 @@ github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= @@ -62,6 +66,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=