Skip to content

Commit

Permalink
Admin and internal endpoints implementation (#20)
Browse files Browse the repository at this point in the history
## 📝 Summary

Implementation for admin endpoints to register new multioperator nodes.
Admin API is served under different port and is implemented for internal
use, it contains all endpoints needed to properly configure new builder
node.

## ⛱ Motivation and Context

To avoid working directly with database (postgres) and secrets manager
(aws secrets manager) we implement admin API that validates data and
puts into the db properly.

## 📚 References

---

## ✅ I have run these commands

* [x] `make lint`
* [x] `make test`
* [x] `go mod tidy`
  • Loading branch information
TymKh authored Nov 20, 2024
1 parent f17ab8b commit c604578
Show file tree
Hide file tree
Showing 18 changed files with 1,047 additions and 45 deletions.
90 changes: 87 additions & 3 deletions adapters/database/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ func (s *Service) GetActiveConfigForBuilder(ctx context.Context, builderName str
SELECT * FROM builder_configs
WHERE builder_name = $1 AND is_active = true
`, builderName)
if errors.Is(err, sql.ErrNoRows) {
return nil, domain.ErrNotFound
}
return config.Config, err
}

Expand Down Expand Up @@ -218,18 +221,99 @@ func (s *Service) GetActiveBuildersWithServiceCredentials(ctx context.Context) (

// LogEvent creates a new log entry in the event_log table.
// It uses hash and attestation_type to fetch the corresponding measurement_id via a subquery.
func (s *Service) LogEvent(ctx context.Context, eventName, builderName, name, attestationType string) error {
func (s *Service) LogEvent(ctx context.Context, eventName, builderName, name string) error {
// Insert new event log entry with a subquery to fetch the measurement_id
_, err := s.DB.ExecContext(ctx, `
INSERT INTO event_log
(event_name, builder_name, measurement_id)
VALUES ($1, $2,
(SELECT id FROM measurements_whitelist WHERE name = $3 AND attestation_type = $4)
(SELECT id FROM measurements_whitelist WHERE name = $3)
)
`, eventName, builderName, name, attestationType)
`, eventName, builderName, name)
if err != nil {
return fmt.Errorf("failed to insert event log for builder %s: %w", builderName, err)
}

return nil
}

func (s *Service) AddMeasurement(ctx context.Context, measurement domain.Measurement, enabled bool) error {
bts, err := json.Marshal(measurement.Measurement)
if err != nil {
return err
}
_, err = s.DB.ExecContext(ctx, `
INSERT INTO measurements_whitelist (name, attestation_type, measurement, is_active)
VALUES ($1, $2, $3, $4)
`, measurement.Name, measurement.AttestationType, bts, enabled)
return err
}

func (s *Service) AddBuilder(ctx context.Context, builder domain.Builder) error {
bIP := pgtype.Inet{}
err := bIP.Set(builder.IPAddress)
if err != nil {
return err
}
_, err = s.DB.ExecContext(ctx, `
INSERT INTO builders (name, ip_address, is_active)
VALUES ($1, $2, $3)
`, builder.Name, bIP, builder.IsActive)
return err
}

func (s *Service) ChangeActiveStatusForBuilder(ctx context.Context, builderName string, isActive bool) error {
_, err := s.DB.ExecContext(ctx, `
UPDATE builders
SET is_active = $1
WHERE name = $2
`, isActive, builderName)
return err
}

func (s *Service) ChangeActiveStatusForMeasurement(ctx context.Context, measurementName string, isActive bool) error {
// NOTE: we currently enforce uniqueness per name and attestation type not just by name
_, err := s.DB.ExecContext(ctx, `
UPDATE measurements_whitelist
SET is_active = $1
WHERE name = $2
`, isActive, measurementName)
return err
}

func (s *Service) AddBuilderConfig(ctx context.Context, builderName string, config json.RawMessage) error {
// Start a transaction
tx, err := s.DB.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}() // Rollback the transaction if it's not committed

// Deactivate any previous configurations for this builder
_, err = tx.Exec(`
UPDATE builder_configs
SET is_active = false, updated_at = NOW()
WHERE builder_name = $1 AND is_active = true
`, builderName)
if err != nil {
return fmt.Errorf("failed to deactivate previous configs for builder %s: %w", builderName, err)
}

// Insert the new configuration as active
_, err = tx.Exec(`
INSERT INTO builder_configs (builder_name, config, is_active)
VALUES ($1, $2, true)
`, builderName, config)
if err != nil {
return fmt.Errorf("failed to insert new config for builder %s: %w", builderName, err)
}

// Commit the transaction
if err = tx.Commit(); err != nil {
return fmt.Errorf("failed to commit transaction for builder %s: %w", builderName, err)
}

return nil
}
62 changes: 62 additions & 0 deletions adapters/database/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package database

import (
"context"
"encoding/json"
"net"
"os"
"testing"

"github.com/flashbots/builder-hub/domain"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -40,3 +42,63 @@ func TestGetBuilder(t *testing.T) {
})
})
}

func TestAdminFlow(t *testing.T) {
//if os.Getenv("RUN_DB_TESTS") != "1" {
// t.Skip("skipping test; RUN_DB_TESTS is not set to 1")
//}
dbService, err := NewDatabaseService("postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable")
if err != nil {
t.Errorf("NewDatabaseService() = %v; want nil", err)
}
_, err = dbService.DB.Exec("TRUNCATE TABLE public.builders CASCADE")
require.NoError(t, err)
_, err = dbService.DB.Exec("TRUNCATE TABLE public.measurements_whitelist CASCADE")
require.NoError(t, err)

t.Run("AdminFlow", func(t *testing.T) {
t.Run("add measurement", func(t *testing.T) {
err := dbService.AddMeasurement(context.Background(), *domain.NewMeasurement("test-measurement-1", "test-type", map[string]domain.SingleMeasurement{"test": {Expected: "0x1234"}}), false)
require.NoError(t, err)
})
t.Run("add builder", func(t *testing.T) {
builder := domain.Builder{
Name: "test-builder",
IPAddress: net.ParseIP("127.0.0.1"),
IsActive: false,
}
err := dbService.AddBuilder(context.Background(), builder)
require.NoError(t, err)
})
t.Run("add builder config", func(t *testing.T) {
conf := `{"key1":"value1", "key2":"value2"}`
err := dbService.AddBuilderConfig(context.Background(), "test-builder", json.RawMessage(conf))
require.NoError(t, err)
})
t.Run("get builders", func(t *testing.T) {
builders, err := dbService.GetActiveBuildersWithServiceCredentials(context.Background())
require.NoError(t, err)
require.Len(t, builders, 0)
})
t.Run("activate measurements", func(t *testing.T) {
err := dbService.ChangeActiveStatusForMeasurement(context.Background(), "test-measurement-1", true)
require.NoError(t, err)
})
t.Run("activate builder", func(t *testing.T) {
err := dbService.ChangeActiveStatusForBuilder(context.Background(), "test-builder", true)
require.NoError(t, err)
})
t.Run("get builders", func(t *testing.T) {
builders, err := dbService.GetActiveBuildersWithServiceCredentials(context.Background())
require.NoError(t, err)
require.Len(t, builders, 1)
require.Equal(t, builders[0].Builder.Name, "test-builder")
})
t.Run("get measurements", func(t *testing.T) {
measurements, err := dbService.GetActiveMeasurements(context.Background())
require.NoError(t, err)
require.Len(t, measurements, 1)
require.Equal(t, measurements[0].Name, "test-measurement-1")
})
})
}
38 changes: 37 additions & 1 deletion adapters/secrets/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (s *Service) GetSecretValues(builderName string) (map[string]string, error)
if err != nil {
return nil, err
}
defaultStr := secretData["default"]
defaultStr := secretData["default"] // TODO: figured out that merging here is suboptimal and should be done in the application layer
defaultSecrets := make(map[string]string)
err = json.Unmarshal([]byte(defaultStr), &defaultSecrets)
if err != nil {
Expand All @@ -61,6 +61,42 @@ func (s *Service) GetSecretValues(builderName string) (map[string]string, error)
return MergeSecrets(defaultSecrets, builderSecrets), nil
}

func (s *Service) SetSecretValues(builderName string, values map[string]string) error {
input := &secretsmanager.GetSecretValueInput{
SecretId: aws.String(s.secretName),
}

result, err := s.sm.GetSecretValue(input)
if err != nil {
return err
}
secretData := make(map[string]string)
err = json.Unmarshal([]byte(*result.SecretString), &secretData)
if err != nil {
return err
}

marshalValues, err := json.Marshal(values)
if err != nil {
return err
}
secretData[builderName] = string(marshalValues)
newSecretString, err := json.Marshal(secretData)
if err != nil {
return err
}

sv := &secretsmanager.PutSecretValueInput{
SecretId: aws.String(s.secretName),
SecretString: aws.String(string(newSecretString)),
}
_, err = s.sm.PutSecretValue(sv)
if err != nil {
return err
}
return nil
}

func MergeSecrets(defaultSecrets, secrets map[string]string) map[string]string {
// merge secrets
res := make(map[string]string)
Expand Down
21 changes: 21 additions & 0 deletions application/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,24 @@ func TestMerge(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "test_value_1", cfg.OrderflowProxy.FlashbotsOfSigningKey)
}

func TestFlatten(t *testing.T) {
jsonBytes := []byte(`{
"user": {
"name": "Alice",
"details": {
"age": "30",
"address": {
"city": "Wonderland"
}
},
"tags": ["admin", "user"]
},
"smb":{"smt":[{"url":"test_value_2"}]}
}`)
flatMap, err := FlattenJSONFromBytes(jsonBytes)
require.NoError(t, err)
require.Equal(t, "Alice", flatMap["user.name"])
require.Equal(t, "test_value_2", flatMap["smb.smt.[0].url"])
}
72 changes: 72 additions & 0 deletions application/config_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ package application

import (
"encoding/json"
"errors"
"strconv"
"strings"

"github.com/buger/jsonparser"
)

var ErrNonStringSecret = errors.New("secret value is not a string")

func MergeConfigSecrets(config json.RawMessage, secrets map[string]string) (json.RawMessage, error) {
// merge config and secrets
bts := []byte(config)
Expand All @@ -22,3 +26,71 @@ func MergeConfigSecrets(config json.RawMessage, secrets map[string]string) (json
}
return bts, nil
}

// Recursive function to flatten JSON objects
func flattenJSON(data map[string]interface{}, prefix string, flatMap map[string]string) error {
for key, value := range data {
newKey := key
if prefix != "" {
newKey = prefix + "." + key
}

switch v := value.(type) {
case map[string]interface{}:
err := flattenJSON(v, newKey, flatMap)
if err != nil {
return err
}
case []interface{}:
err := flattenArray(v, newKey, flatMap)
if err != nil {
return err
}
case string:
flatMap[newKey] = v
default:
// skipping
return ErrNonStringSecret
}
}
return nil
}

// Recursive function to flatten JSON arrays
func flattenArray(data []interface{}, prefix string, flatMap map[string]string) error {
for i, value := range data {
newKey := prefix + "." + "[" + strconv.Itoa(i) + "]"
switch v := value.(type) {
case map[string]interface{}:
err := flattenJSON(v, newKey, flatMap)
if err != nil {
return err
}
case []interface{}:
err := flattenArray(v, newKey, flatMap)
if err != nil {
return err
}
case string:
flatMap[newKey] = v
default:
return ErrNonStringSecret
}
}
return nil
}

// FlattenJSONFromBytes is the main function to process JSON from []byte input
func FlattenJSONFromBytes(jsonBytes []byte) (map[string]string, error) {
var data map[string]interface{}
if err := json.Unmarshal(jsonBytes, &data); err != nil {
return nil, err
}

flatMap := make(map[string]string)
err := flattenJSON(data, "", flatMap)
if err != nil {
return nil, err
}
return flatMap, nil
}
5 changes: 5 additions & 0 deletions application/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type BuilderDataAccessor interface {
GetBuilderByIP(ip net.IP) (*domain.Builder, error)
GetActiveConfigForBuilder(ctx context.Context, builderName string) (json.RawMessage, error)
RegisterCredentialsForBuilder(ctx context.Context, builderName, service, tlsCert string, ecdsaPubKey []byte, measurementName, attestationType string) error
LogEvent(ctx context.Context, eventName, builderName, name string) error
}

type SecretAccessor interface {
Expand All @@ -39,6 +40,10 @@ func (b *BuilderHub) GetActiveBuilders(ctx context.Context) ([]domain.BuilderWit
return b.dataAccessor.GetActiveBuildersWithServiceCredentials(ctx)
}

func (b *BuilderHub) LogEvent(ctx context.Context, eventName, builderName, name string) error {
return b.dataAccessor.LogEvent(ctx, eventName, builderName, name)
}

func (b *BuilderHub) RegisterCredentialsForBuilder(ctx context.Context, builderName, service, tlsCert string, ecdsaPubKey []byte, measurementName, attestationType string) error {
return b.dataAccessor.RegisterCredentialsForBuilder(ctx, builderName, service, tlsCert, ecdsaPubKey, measurementName, attestationType)
}
Expand Down
Loading

0 comments on commit c604578

Please sign in to comment.