Skip to content
34 changes: 34 additions & 0 deletions coordinator/conf/config_proxy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"proxy_manager": {
"proxy_cli": {
"proxy_name": "proxy_name",
"secret": "client private key"
},
"auth": {
"secret": "proxy secret key",
"challenge_expire_duration_sec": 3600,
"login_expire_duration_sec": 3600
},
"verifier": {
"min_prover_version": "v4.4.45",
"verifiers": [
{
"assets_path": "assets",
"fork_name": "euclidV2"
},
{
"assets_path": "assets",
"fork_name": "feynman"
}
]
}
},
"coordinators": {
"sepolia": {
"base_url": "http://localhost:8555",
"retry_count": 10,
"retry_wait_time_sec": 10,
"connection_timeout_sec": 30
}
}
}
72 changes: 72 additions & 0 deletions coordinator/internal/config/proxy_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package config

import (
"encoding/json"
"os"
"path/filepath"

"scroll-tech/common/utils"
)

// Proxy loads proxy configuration items.
type ProxyManager struct {
// Zk verifier config help to confine the connected prover.
Verifier *VerifierConfig `json:"verifier"`
Client *ProxyClient `json:"proxy_cli"`
Auth *Auth `json:"auth"`
}

func (m *ProxyManager) Normalize() {
if m.Client.Secret == "" {
m.Client.Secret = m.Auth.Secret
}

if m.Client.ProxyVersion == "" {
m.Client.ProxyVersion = m.Verifier.MinProverVersion
}
}

// Proxy client configuration for connect to upstream as a client
type ProxyClient struct {
ProxyName string `json:"proxy_name"`
ProxyVersion string `json:"proxy_version,omitempty"`
Secret string `json:"secret,omitempty"`
}

// Coordinator configuration
type UpStream struct {
BaseUrl string `json:"base_url"`
RetryCount uint `json:"retry_count"`
RetryWaitTime uint `json:"retry_wait_time_sec"`
ConnectionTimeoutSec uint `json:"connection_timeout_sec"`
}

// Config load configuration items.
type ProxyConfig struct {
ProxyManager *ProxyManager `json:"proxy_manager"`
ProxyName string `json:"proxy_name"`
Auth *Auth `json:"auth"`
Coordinators map[string]*UpStream `json:"coondiators"`
}

// NewConfig returns a new instance of Config.
func NewProxyConfig(file string) (*ProxyConfig, error) {
buf, err := os.ReadFile(filepath.Clean(file))
if err != nil {
return nil, err
}

cfg := &ProxyConfig{}
err = json.Unmarshal(buf, cfg)
if err != nil {
return nil, err
}

// Override config with environment variables
err = utils.OverrideConfigWithEnv(cfg, "SCROLL_COORDINATOR_PROXY")
if err != nil {
return nil, err
}

return cfg, nil
}
43 changes: 30 additions & 13 deletions coordinator/internal/controller/api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,47 @@ type AuthController struct {
// NewAuthController returns an LoginController instance
func NewAuthController(db *gorm.DB, cfg *config.Config, vf *verifier.Verifier) *AuthController {
return &AuthController{
loginLogic: auth.NewLoginLogic(db, cfg, vf),
loginLogic: auth.NewLoginLogic(db, cfg.ProverManager.Verifier, vf),
}
}

// Login the api controller for login
// Login the api controller for login, used as the Authenticator in JWT
// It can work in two mode: full process for normal login, or if login request
// is posted from proxy, run a simpler process to login a client
func (a *AuthController) Login(c *gin.Context) (interface{}, error) {

// check if the login is post by proxy
var viaProxy bool
if proverType, proverTypeExist := c.Get(types.ProverProviderTypeKey); proverTypeExist {
proverType := uint8(proverType.(float64))
viaProxy = proverType == types.ProverProviderTypeProxy
}

var login types.LoginParameter
if err := c.ShouldBind(&login); err != nil {
return "", fmt.Errorf("missing the public_key, err:%w", err)
}

// check login parameter's token is equal to bearer token, the Authorization must be existed
// if not exist, the jwt token will intercept it
brearToken := c.GetHeader("Authorization")
if brearToken != "Bearer "+login.Message.Challenge {
return "", errors.New("check challenge failure for the not equal challenge string")
// if not, process with normal login
if !viaProxy {
// check login parameter's token is equal to bearer token, the Authorization must be existed
// if not exist, the jwt token will intercept it
brearToken := c.GetHeader("Authorization")
if brearToken != "Bearer "+login.Message.Challenge {
return "", errors.New("check challenge failure for the not equal challenge string")
}

if err := auth.VerifyMsg(&login); err != nil {
return "", err
}

// check the challenge is used, if used, return failure
if err := a.loginLogic.InsertChallengeString(c, login.Message.Challenge); err != nil {
return "", fmt.Errorf("login insert challenge string failure:%w", err)
}
}

if err := a.loginLogic.Check(&login); err != nil {
if err := a.loginLogic.CompatiblityCheck(&login); err != nil {
return "", fmt.Errorf("check the login parameter failure: %w", err)
}

Expand All @@ -49,11 +71,6 @@ func (a *AuthController) Login(c *gin.Context) (interface{}, error) {
return "", fmt.Errorf("prover hard fork name failure:%w", err)
}

// check the challenge is used, if used, return failure
if err := a.loginLogic.InsertChallengeString(c, login.Message.Challenge); err != nil {
return "", fmt.Errorf("login insert challenge string failure:%w", err)
}

returnData := types.LoginParameterWithHardForkName{
HardForkName: hardForkNames,
LoginParameter: login,
Expand Down
168 changes: 168 additions & 0 deletions coordinator/internal/controller/proxy/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package proxy

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/gin-gonic/gin"

"scroll-tech/coordinator/internal/config"
"scroll-tech/coordinator/internal/types"
)

type ClientHelper interface {
GenLoginParam(string) (*types.LoginParameter, error)
OnResp(*upClient, *http.Response)
}

// Client wraps an http client with a preset host for coordinator API calls
type upClient struct {
httpClient *http.Client
baseURL string
loginToken string
helper ClientHelper
}

// NewClient creates a new Client with the specified host
func newUpClient(cfg *config.UpStream, helper ClientHelper) *upClient {
return &upClient{
httpClient: &http.Client{
Timeout: time.Duration(cfg.ConnectionTimeoutSec) * time.Second,
},
baseURL: cfg.BaseUrl,
helper: helper,
}
}

// FullLogin performs the complete login process: get challenge then login
func (c *upClient) Login(ctx context.Context) (*types.LoginSchema, error) {
// Step 1: Get challenge
url := fmt.Sprintf("%s/coordinator/v1/challenge", c.baseURL)

req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create challenge request: %w", err)
}

challengeResp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to get challenge: %w", err)
}
defer challengeResp.Body.Close()

if challengeResp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("challenge request failed with status: %d", challengeResp.StatusCode)
}

// Step 2: Parse challenge response
var loginSchema types.LoginSchema
if err := json.NewDecoder(challengeResp.Body).Decode(&loginSchema); err != nil {
return nil, fmt.Errorf("failed to parse challenge response: %w", err)
}

// Step 3: Use the token from challenge as Bearer token for login
url = fmt.Sprintf("%s/coordinator/v1/login", c.baseURL)

param, err := c.helper.GenLoginParam(loginSchema.Token)
if err != nil {
return nil, fmt.Errorf("failed to setup login parameter: %w", err)
}

jsonData, err := json.Marshal(param)
if err != nil {
return nil, fmt.Errorf("failed to marshal login parameter: %w", err)
}

req, err = http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create login request: %w", err)
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+loginSchema.Token)

loginResp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to perform login request: %w", err)
}

// Parse login response as LoginSchema and store the token
if loginResp.StatusCode == http.StatusOK {
var loginResult types.LoginSchema
if err := json.NewDecoder(loginResp.Body).Decode(&loginResult); err == nil {
c.loginToken = loginResult.Token
}
// Note: Body is consumed after decoding, caller should not read it again
return &loginResult, nil
}

return nil, fmt.Errorf("login request failed with status: %d", loginResp.StatusCode)
}

// ProxyLogin makes a POST request to /v1/proxy_login with LoginParameter
func (c *upClient) ProxyLogin(ctx *gin.Context, param types.LoginParameter) (*http.Response, error) {
url := fmt.Sprintf("%s/coordinator/v1/proxy_login", c.baseURL)

jsonData, err := json.Marshal(param)
if err != nil {
return nil, fmt.Errorf("failed to marshal proxy login parameter: %w", err)
}

req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create proxy login request: %w", err)
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.loginToken)

return c.httpClient.Do(req)
}

// GetTask makes a POST request to /v1/get_task with GetTaskParameter
func (c *upClient) GetTask(ctx *gin.Context, param types.GetTaskParameter, token string) (*http.Response, error) {
url := fmt.Sprintf("%s/coordinator/v1/get_task", c.baseURL)

jsonData, err := json.Marshal(param)
if err != nil {
return nil, fmt.Errorf("failed to marshal get task parameter: %w", err)
}

req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create get task request: %w", err)
}

req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}

return c.httpClient.Do(req)
}

// SubmitProof makes a POST request to /v1/submit_proof with SubmitProofParameter
func (c *upClient) SubmitProof(ctx *gin.Context, param types.SubmitProofParameter, token string) (*http.Response, error) {
url := fmt.Sprintf("%s/coordinator/v1/submit_proof", c.baseURL)

jsonData, err := json.Marshal(param)
if err != nil {
return nil, fmt.Errorf("failed to marshal submit proof parameter: %w", err)
}

req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create submit proof request: %w", err)
}

req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}

return c.httpClient.Do(req)
}
Loading