From 5dbb5c5fb7a2fcdc323cb743e2b54d20ed5454c8 Mon Sep 17 00:00:00 2001 From: Ho Date: Sun, 24 Aug 2025 14:54:54 +0900 Subject: [PATCH 01/13] extend api for proxy --- coordinator/internal/controller/api/auth.go | 41 +++++++++++++++------ coordinator/internal/logic/auth/login.go | 24 ++++++++---- coordinator/internal/types/prover.go | 4 ++ 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/coordinator/internal/controller/api/auth.go b/coordinator/internal/controller/api/auth.go index b38f0d827a..cdbb8225d8 100644 --- a/coordinator/internal/controller/api/auth.go +++ b/coordinator/internal/controller/api/auth.go @@ -26,21 +26,43 @@ func NewAuthController(db *gorm.DB, cfg *config.Config, vf *verifier.Verifier) * } } -// 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 := a.loginLogic.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) } @@ -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, diff --git a/coordinator/internal/logic/auth/login.go b/coordinator/internal/logic/auth/login.go index f5c01a2dfe..0d9222dff6 100644 --- a/coordinator/internal/logic/auth/login.go +++ b/coordinator/internal/logic/auth/login.go @@ -50,13 +50,19 @@ func (l *LoginLogic) InsertChallengeString(ctx *gin.Context, challenge string) e return l.challengeOrm.InsertChallenge(ctx.Copy(), challenge) } -func (l *LoginLogic) Check(login *types.LoginParameter) error { +// Verify the completeness of login message +func (l *LoginLogic) VerifyMsg(login *types.LoginParameter) error { verify, err := login.Verify() if err != nil || !verify { log.Error("auth message verify failure", "prover_name", login.Message.ProverName, "prover_version", login.Message.ProverVersion, "message", login.Message) return errors.New("auth message verify failure") } + return nil +} + +// Check if the login client is compatible with the setting in coordinator +func (l *LoginLogic) CompatiblityCheck(login *types.LoginParameter) error { if !version.CheckScrollRepoVersion(login.Message.ProverVersion, l.cfg.ProverManager.Verifier.MinProverVersion) { return fmt.Errorf("incompatible prover version. please upgrade your prover, minimum allowed version: %s, actual version: %s", l.cfg.ProverManager.Verifier.MinProverVersion, login.Message.ProverVersion) @@ -80,14 +86,16 @@ func (l *LoginLogic) Check(login *types.LoginParameter) error { } } - if login.Message.ProverProviderType != types.ProverProviderTypeInternal && login.Message.ProverProviderType != types.ProverProviderTypeExternal { + switch login.Message.ProverProviderType { + case types.ProverProviderTypeInternal: + case types.ProverProviderTypeExternal: + case types.ProverProviderTypeProxy: + case types.ProverProviderTypeUndefined: // for backward compatibility, set ProverProviderType as internal - if login.Message.ProverProviderType == types.ProverProviderTypeUndefined { - login.Message.ProverProviderType = types.ProverProviderTypeInternal - } else { - log.Error("invalid prover_provider_type", "value", login.Message.ProverProviderType, "prover name", login.Message.ProverName, "prover version", login.Message.ProverVersion) - return errors.New("invalid prover provider type.") - } + login.Message.ProverProviderType = types.ProverProviderTypeInternal + default: + log.Error("invalid prover_provider_type", "value", login.Message.ProverProviderType, "prover name", login.Message.ProverName, "prover version", login.Message.ProverVersion) + return errors.New("invalid prover provider type.") } return nil diff --git a/coordinator/internal/types/prover.go b/coordinator/internal/types/prover.go index 048fac00a2..4254c673d5 100644 --- a/coordinator/internal/types/prover.go +++ b/coordinator/internal/types/prover.go @@ -64,6 +64,8 @@ func (r ProverProviderType) String() string { return "prover provider type internal" case ProverProviderTypeExternal: return "prover provider type external" + case ProverProviderTypeProxy: + return "prover provider type proxy" default: return fmt.Sprintf("prover provider type: %d", r) } @@ -76,4 +78,6 @@ const ( ProverProviderTypeInternal // ProverProviderTypeExternal is an external prover provider type ProverProviderTypeExternal + // ProverProviderTypeProxy is an proxy prover provider type + ProverProviderTypeProxy = 3 ) From 1f2b85767179ee14d92e59b8debb30f6474fdd50 Mon Sep 17 00:00:00 2001 From: Ho Date: Sun, 24 Aug 2025 15:35:51 +0900 Subject: [PATCH 02/13] add proxy_login route --- coordinator/internal/route/route.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coordinator/internal/route/route.go b/coordinator/internal/route/route.go index 9e9eef076e..2a0383aa8d 100644 --- a/coordinator/internal/route/route.go +++ b/coordinator/internal/route/route.go @@ -34,6 +34,7 @@ func v1(router *gin.RouterGroup, conf *config.Config) { // need jwt token api r.Use(loginMiddleware.MiddlewareFunc()) { + r.POST("/proxy_login", loginMiddleware.LoginHandler) r.POST("/get_task", api.GetTask.GetTasks) r.POST("/submit_proof", api.SubmitProof.SubmitProof) } From 9796d16f6c68d3811303cedafcf0d11816823591 Mon Sep 17 00:00:00 2001 From: Ho Date: Sun, 24 Aug 2025 20:32:11 +0900 Subject: [PATCH 03/13] WIP: update login logic and coordinator client --- coordinator/internal/controller/api/auth.go | 4 +- .../internal/controller/proxy/client.go | 214 ++++++++++++++++++ coordinator/internal/logic/auth/login.go | 26 +-- 3 files changed, 229 insertions(+), 15 deletions(-) create mode 100644 coordinator/internal/controller/proxy/client.go diff --git a/coordinator/internal/controller/api/auth.go b/coordinator/internal/controller/api/auth.go index cdbb8225d8..1a3305d2b1 100644 --- a/coordinator/internal/controller/api/auth.go +++ b/coordinator/internal/controller/api/auth.go @@ -22,7 +22,7 @@ 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), } } @@ -52,7 +52,7 @@ func (a *AuthController) Login(c *gin.Context) (interface{}, error) { return "", errors.New("check challenge failure for the not equal challenge string") } - if err := a.loginLogic.VerifyMsg(&login); err != nil { + if err := auth.VerifyMsg(&login); err != nil { return "", err } diff --git a/coordinator/internal/controller/proxy/client.go b/coordinator/internal/controller/proxy/client.go new file mode 100644 index 0000000000..48d768ba80 --- /dev/null +++ b/coordinator/internal/controller/proxy/client.go @@ -0,0 +1,214 @@ +package proxy + +import ( + "bytes" + "crypto/ecdsa" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/scroll-tech/go-ethereum/common" + "github.com/scroll-tech/go-ethereum/crypto" + + "scroll-tech/coordinator/internal/types" +) + +// Client wraps an http client with a preset host for coordinator API calls +type Client struct { + httpClient *http.Client + host string + loginToken string +} + +// NewClient creates a new Client with the specified host +func NewClient(host string) *Client { + return &Client{ + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + host: host, + } +} + +// NewClientWithHTTPClient creates a new Client with a custom http.Client +func NewClientWithHTTPClient(host string, httpClient *http.Client) *Client { + return &Client{ + httpClient: httpClient, + host: host, + } +} + +// FullLogin performs the complete login process: get challenge then login +func (c *Client) Login(param types.LoginParameter) (*types.LoginSchema, error) { + // Step 1: Get challenge + url := fmt.Sprintf("%s/v1/challenge", c.host) + + 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/v1/login", c.host) + + 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 *Client) ProxyLogin(param types.LoginParameter) (*http.Response, error) { + url := fmt.Sprintf("%s/v1/proxy_login", c.host) + + jsonData, err := json.Marshal(param) + if err != nil { + return nil, fmt.Errorf("failed to marshal proxy login parameter: %w", err) + } + + req, err := http.NewRequest("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 *Client) GetTask(param types.GetTaskParameter, token string) (*http.Response, error) { + url := fmt.Sprintf("%s/v1/get_task", c.host) + + jsonData, err := json.Marshal(param) + if err != nil { + return nil, fmt.Errorf("failed to marshal get task parameter: %w", err) + } + + req, err := http.NewRequest("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 *Client) SubmitProof(param types.SubmitProofParameter, token string) (*http.Response, error) { + url := fmt.Sprintf("%s/v1/submit_proof", c.host) + + jsonData, err := json.Marshal(param) + if err != nil { + return nil, fmt.Errorf("failed to marshal submit proof parameter: %w", err) + } + + req, err := http.NewRequest("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) +} + +// transformToValidPrivateKey safely transforms arbitrary bytes into valid private key bytes +func (c *Client) buildPrivateKey(inputBytes []byte) (*ecdsa.PrivateKey, error) { + // Try appending bytes from 0x0 to 0x20 until we get a valid private key + for appendByte := byte(0x0); appendByte <= 0x20; appendByte++ { + // Append the byte to input + extendedBytes := append(inputBytes, appendByte) + + // Calculate 256-bit hash + hash := crypto.Keccak256(extendedBytes) + + // Try to create private key from hash + if k, err := crypto.ToECDSA(hash); err == nil { + return k, nil + } + } + + return nil, fmt.Errorf("failed to generate valid private key from input bytes") +} + +func (c *Client) generateLoginParameter(privateKeyBytes []byte, challenge string) (*types.LoginParameter, error) { + // Generate private key + privKey, err := c.buildPrivateKey(privateKeyBytes) + if err != nil { + return nil, err + } + + // Generate public key string + publicKeyHex := common.Bytes2Hex(crypto.CompressPubkey(&privKey.PublicKey)) + + // Create login parameter with proxy settings + loginParam := &types.LoginParameter{ + Message: types.Message{ + Challenge: challenge, + ProverName: "proxy", + ProverVersion: "proxy", + ProverProviderType: types.ProverProviderTypeProxy, + ProverTypes: []types.ProverType{}, // Default empty + VKs: []string{}, // Default empty + }, + PublicKey: publicKeyHex, + } + + // Sign the message with the private key + if err := loginParam.SignWithKey(privKey); err != nil { + return nil, fmt.Errorf("failed to sign login parameter: %w", err) + } + + return loginParam, nil +} diff --git a/coordinator/internal/logic/auth/login.go b/coordinator/internal/logic/auth/login.go index 0d9222dff6..35350ef1ea 100644 --- a/coordinator/internal/logic/auth/login.go +++ b/coordinator/internal/logic/auth/login.go @@ -19,7 +19,7 @@ import ( // LoginLogic the auth logic type LoginLogic struct { - cfg *config.Config + cfg *config.VerifierConfig challengeOrm *orm.Challenge openVmVks map[string]struct{} @@ -28,30 +28,25 @@ type LoginLogic struct { } // NewLoginLogic new a LoginLogic -func NewLoginLogic(db *gorm.DB, cfg *config.Config, vf *verifier.Verifier) *LoginLogic { +func NewLoginLogic(db *gorm.DB, vcfg *config.VerifierConfig, vf *verifier.Verifier) *LoginLogic { proverVersionHardForkMap := make(map[string][]string) var hardForks []string - for _, cfg := range cfg.ProverManager.Verifier.Verifiers { + for _, cfg := range vcfg.Verifiers { hardForks = append(hardForks, cfg.ForkName) } - proverVersionHardForkMap[cfg.ProverManager.Verifier.MinProverVersion] = hardForks + proverVersionHardForkMap[vcfg.MinProverVersion] = hardForks return &LoginLogic{ - cfg: cfg, + cfg: vcfg, openVmVks: vf.OpenVMVkMap, challengeOrm: orm.NewChallenge(db), proverVersionHardForkMap: proverVersionHardForkMap, } } -// InsertChallengeString insert and check the challenge string is existed -func (l *LoginLogic) InsertChallengeString(ctx *gin.Context, challenge string) error { - return l.challengeOrm.InsertChallenge(ctx.Copy(), challenge) -} - // Verify the completeness of login message -func (l *LoginLogic) VerifyMsg(login *types.LoginParameter) error { +func VerifyMsg(login *types.LoginParameter) error { verify, err := login.Verify() if err != nil || !verify { log.Error("auth message verify failure", "prover_name", login.Message.ProverName, @@ -61,11 +56,16 @@ func (l *LoginLogic) VerifyMsg(login *types.LoginParameter) error { return nil } +// InsertChallengeString insert and check the challenge string is existed +func (l *LoginLogic) InsertChallengeString(ctx *gin.Context, challenge string) error { + return l.challengeOrm.InsertChallenge(ctx.Copy(), challenge) +} + // Check if the login client is compatible with the setting in coordinator func (l *LoginLogic) CompatiblityCheck(login *types.LoginParameter) error { - if !version.CheckScrollRepoVersion(login.Message.ProverVersion, l.cfg.ProverManager.Verifier.MinProverVersion) { - return fmt.Errorf("incompatible prover version. please upgrade your prover, minimum allowed version: %s, actual version: %s", l.cfg.ProverManager.Verifier.MinProverVersion, login.Message.ProverVersion) + if !version.CheckScrollRepoVersion(login.Message.ProverVersion, l.cfg.MinProverVersion) { + return fmt.Errorf("incompatible prover version. please upgrade your prover, minimum allowed version: %s, actual version: %s", l.cfg.MinProverVersion, login.Message.ProverVersion) } vks := make(map[string]struct{}) From 412ad56a64677e9175b8a2d37523d35a9ec4723c Mon Sep 17 00:00:00 2001 From: Ho Date: Sun, 24 Aug 2025 20:43:40 +0900 Subject: [PATCH 04/13] extend loginlogic --- coordinator/internal/logic/auth/login.go | 27 +++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/coordinator/internal/logic/auth/login.go b/coordinator/internal/logic/auth/login.go index 35350ef1ea..12830d6d4d 100644 --- a/coordinator/internal/logic/auth/login.go +++ b/coordinator/internal/logic/auth/login.go @@ -1,6 +1,7 @@ package auth import ( + "context" "errors" "fmt" "strings" @@ -20,15 +21,35 @@ import ( // LoginLogic the auth logic type LoginLogic struct { cfg *config.VerifierConfig - challengeOrm *orm.Challenge + deduplicator ChallengeDeduplicator openVmVks map[string]struct{} proverVersionHardForkMap map[string][]string } +type ChallengeDeduplicator interface { + InsertChallenge(ctx context.Context, challengeString string) error +} + +type SimpleDeduplicator struct { +} + +func (s *SimpleDeduplicator) InsertChallenge(ctx context.Context, challengeString string) error { + return nil +} + +// NewLoginLogicWithSimpleDEduplicator new a LoginLogic, do not use db to deduplicate challege +func NewLoginLogicWithSimpleDEduplicator(vcfg *config.VerifierConfig, vf *verifier.Verifier) *LoginLogic { + return newLoginLogic(&SimpleDeduplicator{}, vcfg, vf) +} + // NewLoginLogic new a LoginLogic func NewLoginLogic(db *gorm.DB, vcfg *config.VerifierConfig, vf *verifier.Verifier) *LoginLogic { + return newLoginLogic(orm.NewChallenge(db), vcfg, vf) +} + +func newLoginLogic(deduplicator ChallengeDeduplicator, vcfg *config.VerifierConfig, vf *verifier.Verifier) *LoginLogic { proverVersionHardForkMap := make(map[string][]string) var hardForks []string @@ -40,7 +61,7 @@ func NewLoginLogic(db *gorm.DB, vcfg *config.VerifierConfig, vf *verifier.Verifi return &LoginLogic{ cfg: vcfg, openVmVks: vf.OpenVMVkMap, - challengeOrm: orm.NewChallenge(db), + deduplicator: deduplicator, proverVersionHardForkMap: proverVersionHardForkMap, } } @@ -58,7 +79,7 @@ func VerifyMsg(login *types.LoginParameter) error { // InsertChallengeString insert and check the challenge string is existed func (l *LoginLogic) InsertChallengeString(ctx *gin.Context, challenge string) error { - return l.challengeOrm.InsertChallenge(ctx.Copy(), challenge) + return l.deduplicator.InsertChallenge(ctx.Copy(), challenge) } // Check if the login client is compatible with the setting in coordinator From 3adb2e0a1b123c49179711cf8b89cd8041651750 Mon Sep 17 00:00:00 2001 From: Ho Date: Sun, 24 Aug 2025 21:18:13 +0900 Subject: [PATCH 05/13] WIP: controller --- coordinator/internal/config/proxy_config.go | 53 +++++++++++++++++++ .../internal/controller/proxy/client.go | 27 ++++------ .../internal/controller/proxy/controller.go | 41 ++++++++++++++ 3 files changed, 104 insertions(+), 17 deletions(-) create mode 100644 coordinator/internal/config/proxy_config.go create mode 100644 coordinator/internal/controller/proxy/controller.go diff --git a/coordinator/internal/config/proxy_config.go b/coordinator/internal/config/proxy_config.go new file mode 100644 index 0000000000..f94ef54c2e --- /dev/null +++ b/coordinator/internal/config/proxy_config.go @@ -0,0 +1,53 @@ +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"` +} + +// 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 +} diff --git a/coordinator/internal/controller/proxy/client.go b/coordinator/internal/controller/proxy/client.go index 48d768ba80..17c394db68 100644 --- a/coordinator/internal/controller/proxy/client.go +++ b/coordinator/internal/controller/proxy/client.go @@ -11,38 +11,31 @@ import ( "github.com/scroll-tech/go-ethereum/common" "github.com/scroll-tech/go-ethereum/crypto" + "scroll-tech/coordinator/internal/config" "scroll-tech/coordinator/internal/types" ) // Client wraps an http client with a preset host for coordinator API calls type Client struct { httpClient *http.Client - host string + baseURL string loginToken string } // NewClient creates a new Client with the specified host -func NewClient(host string) *Client { +func NewClient(cfg *config.UpStream) *Client { return &Client{ httpClient: &http.Client{ - Timeout: 30 * time.Second, + Timeout: time.Duration(cfg.ConnectionTimeoutSec) * time.Second, }, - host: host, - } -} - -// NewClientWithHTTPClient creates a new Client with a custom http.Client -func NewClientWithHTTPClient(host string, httpClient *http.Client) *Client { - return &Client{ - httpClient: httpClient, - host: host, + baseURL: cfg.BaseUrl, } } // FullLogin performs the complete login process: get challenge then login func (c *Client) Login(param types.LoginParameter) (*types.LoginSchema, error) { // Step 1: Get challenge - url := fmt.Sprintf("%s/v1/challenge", c.host) + url := fmt.Sprintf("%s/coordinator/v1/challenge", c.baseURL) req, err := http.NewRequest("GET", url, nil) if err != nil { @@ -66,7 +59,7 @@ func (c *Client) Login(param types.LoginParameter) (*types.LoginSchema, error) { } // Step 3: Use the token from challenge as Bearer token for login - url = fmt.Sprintf("%s/v1/login", c.host) + url = fmt.Sprintf("%s/coordinator/v1/login", c.baseURL) jsonData, err := json.Marshal(param) if err != nil { @@ -101,7 +94,7 @@ func (c *Client) Login(param types.LoginParameter) (*types.LoginSchema, error) { // ProxyLogin makes a POST request to /v1/proxy_login with LoginParameter func (c *Client) ProxyLogin(param types.LoginParameter) (*http.Response, error) { - url := fmt.Sprintf("%s/v1/proxy_login", c.host) + url := fmt.Sprintf("%s/coordinator/v1/proxy_login", c.baseURL) jsonData, err := json.Marshal(param) if err != nil { @@ -121,7 +114,7 @@ func (c *Client) ProxyLogin(param types.LoginParameter) (*http.Response, error) // GetTask makes a POST request to /v1/get_task with GetTaskParameter func (c *Client) GetTask(param types.GetTaskParameter, token string) (*http.Response, error) { - url := fmt.Sprintf("%s/v1/get_task", c.host) + url := fmt.Sprintf("%s/coordinator/v1/get_task", c.baseURL) jsonData, err := json.Marshal(param) if err != nil { @@ -143,7 +136,7 @@ func (c *Client) GetTask(param types.GetTaskParameter, token string) (*http.Resp // SubmitProof makes a POST request to /v1/submit_proof with SubmitProofParameter func (c *Client) SubmitProof(param types.SubmitProofParameter, token string) (*http.Response, error) { - url := fmt.Sprintf("%s/v1/submit_proof", c.host) + url := fmt.Sprintf("%s/coordinator/v1/submit_proof", c.baseURL) jsonData, err := json.Marshal(param) if err != nil { diff --git a/coordinator/internal/controller/proxy/controller.go b/coordinator/internal/controller/proxy/controller.go new file mode 100644 index 0000000000..02f6350d7d --- /dev/null +++ b/coordinator/internal/controller/proxy/controller.go @@ -0,0 +1,41 @@ +package proxy + +import ( + "github.com/scroll-tech/go-ethereum/log" + + "scroll-tech/coordinator/internal/config" + "scroll-tech/coordinator/internal/logic/verifier" +) + +var ( + // GetTask the prover task controller + GetTask *GetTaskController + // SubmitProof the submit proof controller + SubmitProof *SubmitProofController + // Auth the auth controller + Auth *AuthController +) + +// Clients manager a series of thread-safe clients for requesting upstream +// coordinators +type Clients map[string]*Client + +// InitController inits Controller with database +func InitController(cfg *config.ProxyConfig) { + vf, err := verifier.NewVerifier(cfg.ProxyManager.Verifier) + if err != nil { + panic("proof receiver new verifier failure") + } + + log.Info("verifier created", "openVmVerifier", vf.OpenVMVkMap) + + clients := make(map[string]*Client) + + for nm, cfg := range cfg.Coordinators { + clients[nm] = NewClient(cfg) + } + + Auth = NewAuthController(cfg, clients, vf) + // GetTask = NewGetTaskController(cfg, chainCfg, db, vf, reg) + // SubmitProof = NewSubmitProofController(cfg, chainCfg, db, vf, reg) +} From 5c6c225f7628f6ecbc5a9a88a07ede7d7642d8a8 Mon Sep 17 00:00:00 2001 From: Ho Date: Sun, 24 Aug 2025 22:14:22 +0900 Subject: [PATCH 06/13] WIP: config and client controller --- coordinator/internal/config/proxy_config.go | 19 ++++ .../internal/controller/proxy/client.go | 75 +++------------- .../controller/proxy/client_manager.go | 86 +++++++++++++++++++ .../internal/controller/proxy/controller.go | 15 +++- 4 files changed, 128 insertions(+), 67 deletions(-) create mode 100644 coordinator/internal/controller/proxy/client_manager.go diff --git a/coordinator/internal/config/proxy_config.go b/coordinator/internal/config/proxy_config.go index f94ef54c2e..5edb7fc783 100644 --- a/coordinator/internal/config/proxy_config.go +++ b/coordinator/internal/config/proxy_config.go @@ -12,6 +12,25 @@ import ( 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.Auth == nil { + m.Client.Auth = m.Auth + } + + 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"` + Auth *Auth `json:"auth,omitempty"` } // Coordinator configuration diff --git a/coordinator/internal/controller/proxy/client.go b/coordinator/internal/controller/proxy/client.go index 17c394db68..9fe5ea8cb7 100644 --- a/coordinator/internal/controller/proxy/client.go +++ b/coordinator/internal/controller/proxy/client.go @@ -2,29 +2,28 @@ package proxy import ( "bytes" - "crypto/ecdsa" + "context" "encoding/json" "fmt" "net/http" "time" - "github.com/scroll-tech/go-ethereum/common" - "github.com/scroll-tech/go-ethereum/crypto" + "github.com/gin-gonic/gin" "scroll-tech/coordinator/internal/config" "scroll-tech/coordinator/internal/types" ) // Client wraps an http client with a preset host for coordinator API calls -type Client struct { +type upClient struct { httpClient *http.Client baseURL string loginToken string } // NewClient creates a new Client with the specified host -func NewClient(cfg *config.UpStream) *Client { - return &Client{ +func newUpClient(cfg *config.UpStream) *upClient { + return &upClient{ httpClient: &http.Client{ Timeout: time.Duration(cfg.ConnectionTimeoutSec) * time.Second, }, @@ -33,7 +32,7 @@ func NewClient(cfg *config.UpStream) *Client { } // FullLogin performs the complete login process: get challenge then login -func (c *Client) Login(param types.LoginParameter) (*types.LoginSchema, error) { +func (c *upClient) Login(ctx context.Context, param types.LoginParameter) (*types.LoginSchema, error) { // Step 1: Get challenge url := fmt.Sprintf("%s/coordinator/v1/challenge", c.baseURL) @@ -93,7 +92,7 @@ func (c *Client) Login(param types.LoginParameter) (*types.LoginSchema, error) { } // ProxyLogin makes a POST request to /v1/proxy_login with LoginParameter -func (c *Client) ProxyLogin(param types.LoginParameter) (*http.Response, error) { +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) @@ -101,7 +100,7 @@ func (c *Client) ProxyLogin(param types.LoginParameter) (*http.Response, error) return nil, fmt.Errorf("failed to marshal proxy login parameter: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + 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) } @@ -113,7 +112,7 @@ func (c *Client) ProxyLogin(param types.LoginParameter) (*http.Response, error) } // GetTask makes a POST request to /v1/get_task with GetTaskParameter -func (c *Client) GetTask(param types.GetTaskParameter, token string) (*http.Response, error) { +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) @@ -121,7 +120,7 @@ func (c *Client) GetTask(param types.GetTaskParameter, token string) (*http.Resp return nil, fmt.Errorf("failed to marshal get task parameter: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + 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) } @@ -135,7 +134,7 @@ func (c *Client) GetTask(param types.GetTaskParameter, token string) (*http.Resp } // SubmitProof makes a POST request to /v1/submit_proof with SubmitProofParameter -func (c *Client) SubmitProof(param types.SubmitProofParameter, token string) (*http.Response, error) { +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) @@ -143,7 +142,7 @@ func (c *Client) SubmitProof(param types.SubmitProofParameter, token string) (*h return nil, fmt.Errorf("failed to marshal submit proof parameter: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + 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) } @@ -155,53 +154,3 @@ func (c *Client) SubmitProof(param types.SubmitProofParameter, token string) (*h return c.httpClient.Do(req) } - -// transformToValidPrivateKey safely transforms arbitrary bytes into valid private key bytes -func (c *Client) buildPrivateKey(inputBytes []byte) (*ecdsa.PrivateKey, error) { - // Try appending bytes from 0x0 to 0x20 until we get a valid private key - for appendByte := byte(0x0); appendByte <= 0x20; appendByte++ { - // Append the byte to input - extendedBytes := append(inputBytes, appendByte) - - // Calculate 256-bit hash - hash := crypto.Keccak256(extendedBytes) - - // Try to create private key from hash - if k, err := crypto.ToECDSA(hash); err == nil { - return k, nil - } - } - - return nil, fmt.Errorf("failed to generate valid private key from input bytes") -} - -func (c *Client) generateLoginParameter(privateKeyBytes []byte, challenge string) (*types.LoginParameter, error) { - // Generate private key - privKey, err := c.buildPrivateKey(privateKeyBytes) - if err != nil { - return nil, err - } - - // Generate public key string - publicKeyHex := common.Bytes2Hex(crypto.CompressPubkey(&privKey.PublicKey)) - - // Create login parameter with proxy settings - loginParam := &types.LoginParameter{ - Message: types.Message{ - Challenge: challenge, - ProverName: "proxy", - ProverVersion: "proxy", - ProverProviderType: types.ProverProviderTypeProxy, - ProverTypes: []types.ProverType{}, // Default empty - VKs: []string{}, // Default empty - }, - PublicKey: publicKeyHex, - } - - // Sign the message with the private key - if err := loginParam.SignWithKey(privKey); err != nil { - return nil, fmt.Errorf("failed to sign login parameter: %w", err) - } - - return loginParam, nil -} diff --git a/coordinator/internal/controller/proxy/client_manager.go b/coordinator/internal/controller/proxy/client_manager.go new file mode 100644 index 0000000000..302292fb16 --- /dev/null +++ b/coordinator/internal/controller/proxy/client_manager.go @@ -0,0 +1,86 @@ +package proxy + +import ( + "context" + "crypto/ecdsa" + "fmt" + + "github.com/scroll-tech/go-ethereum/common" + "github.com/scroll-tech/go-ethereum/crypto" + + "scroll-tech/coordinator/internal/config" + "scroll-tech/coordinator/internal/types" +) + +type Client interface { + Client(context.Context) *upClient +} + +type ClientManager struct { + cliCfg *config.ProxyClient + cfg *config.UpStream + privKey *ecdsa.PrivateKey +} + +// transformToValidPrivateKey safely transforms arbitrary bytes into valid private key bytes +func buildPrivateKey(inputBytes []byte) (*ecdsa.PrivateKey, error) { + // Try appending bytes from 0x0 to 0x20 until we get a valid private key + for appendByte := byte(0x0); appendByte <= 0x20; appendByte++ { + // Append the byte to input + extendedBytes := append(inputBytes, appendByte) + + // Calculate 256-bit hash + hash := crypto.Keccak256(extendedBytes) + + // Try to create private key from hash + if k, err := crypto.ToECDSA(hash); err == nil { + return k, nil + } + } + + return nil, fmt.Errorf("failed to generate valid private key from input bytes") +} + +func NewClientManager(cliCfg *config.ProxyClient, cfg *config.UpStream) (*ClientManager, error) { + + privKey, err := buildPrivateKey([]byte(cliCfg.Auth.Secret)) + if err != nil { + return nil, err + } + + return &ClientManager{ + privKey: privKey, + cfg: cfg, + cliCfg: cliCfg, + }, nil +} + +func (cliMgr *ClientManager) Client(ctx context.Context) *upClient { + return newUpClient(cliMgr.cfg) +} + +func (cliMgr *ClientManager) generateLoginParameter(privKey []byte, challenge string) (*types.LoginParameter, error) { + + // Generate public key string + publicKeyHex := common.Bytes2Hex(crypto.CompressPubkey(&cliMgr.privKey.PublicKey)) + + // Create login parameter with proxy settings + loginParam := &types.LoginParameter{ + Message: types.Message{ + Challenge: challenge, + ProverName: cliMgr.cliCfg.ProxyName, + ProverVersion: cliMgr.cliCfg.ProxyVersion, + ProverProviderType: types.ProverProviderTypeProxy, + ProverTypes: []types.ProverType{}, // Default empty + VKs: []string{}, // Default empty + }, + PublicKey: publicKeyHex, + } + + // Sign the message with the private key + if err := loginParam.SignWithKey(cliMgr.privKey); err != nil { + return nil, fmt.Errorf("failed to sign login parameter: %w", err) + } + + return loginParam, nil +} diff --git a/coordinator/internal/controller/proxy/controller.go b/coordinator/internal/controller/proxy/controller.go index 02f6350d7d..297f8c68a4 100644 --- a/coordinator/internal/controller/proxy/controller.go +++ b/coordinator/internal/controller/proxy/controller.go @@ -18,10 +18,13 @@ var ( // Clients manager a series of thread-safe clients for requesting upstream // coordinators -type Clients map[string]*Client +type Clients map[string]Client // InitController inits Controller with database func InitController(cfg *config.ProxyConfig) { + // normalize cfg + cfg.ProxyManager.Normalize() + vf, err := verifier.NewVerifier(cfg.ProxyManager.Verifier) if err != nil { panic("proof receiver new verifier failure") @@ -29,10 +32,14 @@ func InitController(cfg *config.ProxyConfig) { log.Info("verifier created", "openVmVerifier", vf.OpenVMVkMap) - clients := make(map[string]*Client) + clients := make(map[string]Client) - for nm, cfg := range cfg.Coordinators { - clients[nm] = NewClient(cfg) + for nm, upCfg := range cfg.Coordinators { + cli, err := NewClientManager(cfg.ProxyManager.Client, upCfg) + if err != nil { + panic("create new client fail") + } + clients[nm] = cli } Auth = NewAuthController(cfg, clients, vf) From 76ecdf064a79bac7a645bd9c9afcde50bcf21d07 Mon Sep 17 00:00:00 2001 From: Ho Date: Sun, 24 Aug 2025 22:14:32 +0900 Subject: [PATCH 07/13] add proxy config sample --- coordinator/conf/config_proxy.json | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 coordinator/conf/config_proxy.json diff --git a/coordinator/conf/config_proxy.json b/coordinator/conf/config_proxy.json new file mode 100644 index 0000000000..39c3ca0a5f --- /dev/null +++ b/coordinator/conf/config_proxy.json @@ -0,0 +1,33 @@ +{ + "proxy_manager": { + "proxy_cli": { + "proxy_name": "proxy_name" + }, + "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 + } + } +} From 0d238d77a6ddb1c0f66030f9154ffb45c9092d80 Mon Sep 17 00:00:00 2001 From: Ho Date: Sun, 24 Aug 2025 22:32:38 +0900 Subject: [PATCH 08/13] WIP: the structure of client manager --- coordinator/internal/controller/proxy/client.go | 16 ++++++++++++++-- .../internal/controller/proxy/client_manager.go | 8 ++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/coordinator/internal/controller/proxy/client.go b/coordinator/internal/controller/proxy/client.go index 9fe5ea8cb7..8850274f1c 100644 --- a/coordinator/internal/controller/proxy/client.go +++ b/coordinator/internal/controller/proxy/client.go @@ -14,25 +14,32 @@ import ( "scroll-tech/coordinator/internal/types" ) +type ClientHelper interface { + GenLoginParam(string) (*types.LoginParameter, error) + OnError(isUnauth bool) +} + // 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) *upClient { +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, param types.LoginParameter) (*types.LoginSchema, error) { +func (c *upClient) Login(ctx context.Context) (*types.LoginSchema, error) { // Step 1: Get challenge url := fmt.Sprintf("%s/coordinator/v1/challenge", c.baseURL) @@ -60,6 +67,11 @@ func (c *upClient) Login(ctx context.Context, param types.LoginParameter) (*type // 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) diff --git a/coordinator/internal/controller/proxy/client_manager.go b/coordinator/internal/controller/proxy/client_manager.go index 302292fb16..399a424fe1 100644 --- a/coordinator/internal/controller/proxy/client_manager.go +++ b/coordinator/internal/controller/proxy/client_manager.go @@ -56,10 +56,14 @@ func NewClientManager(cliCfg *config.ProxyClient, cfg *config.UpStream) (*Client } func (cliMgr *ClientManager) Client(ctx context.Context) *upClient { - return newUpClient(cliMgr.cfg) + return newUpClient(cliMgr.cfg, cliMgr) } -func (cliMgr *ClientManager) generateLoginParameter(privKey []byte, challenge string) (*types.LoginParameter, error) { +func (cliMgr *ClientManager) OnError(isUnauth bool) { + +} + +func (cliMgr *ClientManager) GenLoginParam(challenge string) (*types.LoginParameter, error) { // Generate public key string publicKeyHex := common.Bytes2Hex(crypto.CompressPubkey(&cliMgr.privKey.PublicKey)) From 7b3a65b35b029e93c8622ef0c163168a4f0bb796 Mon Sep 17 00:00:00 2001 From: Ho Date: Sun, 24 Aug 2025 22:41:17 +0900 Subject: [PATCH 09/13] framework for auto login --- .../controller/proxy/client_manager.go | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/coordinator/internal/controller/proxy/client_manager.go b/coordinator/internal/controller/proxy/client_manager.go index 399a424fe1..6893f34dd4 100644 --- a/coordinator/internal/controller/proxy/client_manager.go +++ b/coordinator/internal/controller/proxy/client_manager.go @@ -1,10 +1,11 @@ package proxy import ( - "context" "crypto/ecdsa" "fmt" + "sync" + "github.com/gin-gonic/gin" "github.com/scroll-tech/go-ethereum/common" "github.com/scroll-tech/go-ethereum/crypto" @@ -13,13 +14,18 @@ import ( ) type Client interface { - Client(context.Context) *upClient + Client(*gin.Context) *upClient } type ClientManager struct { cliCfg *config.ProxyClient cfg *config.UpStream privKey *ecdsa.PrivateKey + + cachedCli struct { + sync.RWMutex + cli *upClient + } } // transformToValidPrivateKey safely transforms arbitrary bytes into valid private key bytes @@ -55,8 +61,26 @@ func NewClientManager(cliCfg *config.ProxyClient, cfg *config.UpStream) (*Client }, nil } -func (cliMgr *ClientManager) Client(ctx context.Context) *upClient { - return newUpClient(cliMgr.cfg, cliMgr) +func (cliMgr *ClientManager) doLogin() *upClient { + loginCli := newUpClient(cliMgr.cfg, cliMgr) + + return loginCli +} + +func (cliMgr *ClientManager) Client(ctx *gin.Context) *upClient { + cliMgr.cachedCli.RLock() + if cliMgr.cachedCli.cli != nil { + defer cliMgr.cachedCli.RUnlock() + return cliMgr.cachedCli.cli + } + cliMgr.cachedCli.RUnlock() + cliMgr.cachedCli.Lock() + defer cliMgr.cachedCli.Unlock() + if cliMgr.cachedCli.cli != nil { + return cliMgr.cachedCli.cli + } + + return nil } func (cliMgr *ClientManager) OnError(isUnauth bool) { From 4f878d9231404310a8b7519695747599492807bf Mon Sep 17 00:00:00 2001 From: Ho Date: Sun, 24 Aug 2025 23:05:56 +0900 Subject: [PATCH 10/13] AI step --- .../controller/proxy/client_manager.go | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/coordinator/internal/controller/proxy/client_manager.go b/coordinator/internal/controller/proxy/client_manager.go index 6893f34dd4..b22b9847d5 100644 --- a/coordinator/internal/controller/proxy/client_manager.go +++ b/coordinator/internal/controller/proxy/client_manager.go @@ -1,6 +1,7 @@ package proxy import ( + "context" "crypto/ecdsa" "fmt" "sync" @@ -24,7 +25,9 @@ type ClientManager struct { cachedCli struct { sync.RWMutex - cli *upClient + cli *upClient + completionCtx context.Context + completionDone context.CancelFunc } } @@ -74,13 +77,49 @@ func (cliMgr *ClientManager) Client(ctx *gin.Context) *upClient { return cliMgr.cachedCli.cli } cliMgr.cachedCli.RUnlock() + cliMgr.cachedCli.Lock() - defer cliMgr.cachedCli.Unlock() if cliMgr.cachedCli.cli != nil { + defer cliMgr.cachedCli.Unlock() return cliMgr.cachedCli.cli } - return nil + var completionCtx context.Context + // Check if completion context is set + if cliMgr.cachedCli.completionCtx != nil { + completionCtx = cliMgr.cachedCli.completionCtx + } else { + // Set new completion context and launch login goroutine + ctx, completionDone := context.WithCancel(context.TODO()) + cliMgr.cachedCli.completionCtx = ctx + + // Launch login goroutine + go func() { + defer completionDone() + + loginCli := cliMgr.doLogin() + if loginResult, err := loginCli.Login(context.Background()); err == nil { + loginCli.loginToken = loginResult.Token + + cliMgr.cachedCli.Lock() + cliMgr.cachedCli.cli = loginCli + cliMgr.cachedCli.completionCtx = nil + cliMgr.cachedCli.Unlock() + } + }() + } + cliMgr.cachedCli.Unlock() + + // Wait for completion or request cancellation + select { + case <-ctx.Done(): + return nil + case <-completionCtx.Done(): + cliMgr.cachedCli.Lock() + cli := cliMgr.cachedCli.cli + cliMgr.cachedCli.Unlock() + return cli + } } func (cliMgr *ClientManager) OnError(isUnauth bool) { From 624a7a29b843d89a8643daa246a224670b958e74 Mon Sep 17 00:00:00 2001 From: Ho Date: Mon, 25 Aug 2025 09:35:10 +0900 Subject: [PATCH 11/13] WIP: AI step --- coordinator/conf/config_proxy.json | 3 +- coordinator/internal/config/proxy_config.go | 6 +- .../internal/controller/proxy/client.go | 2 +- .../controller/proxy/client_manager.go | 116 ++++++++++++++---- 4 files changed, 98 insertions(+), 29 deletions(-) diff --git a/coordinator/conf/config_proxy.json b/coordinator/conf/config_proxy.json index 39c3ca0a5f..886c10bf51 100644 --- a/coordinator/conf/config_proxy.json +++ b/coordinator/conf/config_proxy.json @@ -1,7 +1,8 @@ { "proxy_manager": { "proxy_cli": { - "proxy_name": "proxy_name" + "proxy_name": "proxy_name", + "secret": "client private key" }, "auth": { "secret": "proxy secret key", diff --git a/coordinator/internal/config/proxy_config.go b/coordinator/internal/config/proxy_config.go index 5edb7fc783..e2d510a29b 100644 --- a/coordinator/internal/config/proxy_config.go +++ b/coordinator/internal/config/proxy_config.go @@ -17,8 +17,8 @@ type ProxyManager struct { } func (m *ProxyManager) Normalize() { - if m.Client.Auth == nil { - m.Client.Auth = m.Auth + if m.Client.Secret == "" { + m.Client.Secret = m.Auth.Secret } if m.Client.ProxyVersion == "" { @@ -30,7 +30,7 @@ func (m *ProxyManager) Normalize() { type ProxyClient struct { ProxyName string `json:"proxy_name"` ProxyVersion string `json:"proxy_version,omitempty"` - Auth *Auth `json:"auth,omitempty"` + Secret string `json:"secret,omitempty"` } // Coordinator configuration diff --git a/coordinator/internal/controller/proxy/client.go b/coordinator/internal/controller/proxy/client.go index 8850274f1c..92b0e5d3c8 100644 --- a/coordinator/internal/controller/proxy/client.go +++ b/coordinator/internal/controller/proxy/client.go @@ -16,7 +16,7 @@ import ( type ClientHelper interface { GenLoginParam(string) (*types.LoginParameter, error) - OnError(isUnauth bool) + OnResp(*upClient, *http.Response) } // Client wraps an http client with a preset host for coordinator API calls diff --git a/coordinator/internal/controller/proxy/client_manager.go b/coordinator/internal/controller/proxy/client_manager.go index b22b9847d5..bae8954998 100644 --- a/coordinator/internal/controller/proxy/client_manager.go +++ b/coordinator/internal/controller/proxy/client_manager.go @@ -4,18 +4,20 @@ import ( "context" "crypto/ecdsa" "fmt" + "net/http" "sync" + "time" - "github.com/gin-gonic/gin" "github.com/scroll-tech/go-ethereum/common" "github.com/scroll-tech/go-ethereum/crypto" + "github.com/scroll-tech/go-ethereum/log" "scroll-tech/coordinator/internal/config" "scroll-tech/coordinator/internal/types" ) type Client interface { - Client(*gin.Context) *upClient + Client(context.Context) *upClient } type ClientManager struct { @@ -25,9 +27,9 @@ type ClientManager struct { cachedCli struct { sync.RWMutex - cli *upClient - completionCtx context.Context - completionDone context.CancelFunc + cli *upClient + completionCtx context.Context + resultChan chan *upClient } } @@ -52,7 +54,7 @@ func buildPrivateKey(inputBytes []byte) (*ecdsa.PrivateKey, error) { func NewClientManager(cliCfg *config.ProxyClient, cfg *config.UpStream) (*ClientManager, error) { - privKey, err := buildPrivateKey([]byte(cliCfg.Auth.Secret)) + privKey, err := buildPrivateKey([]byte(cliCfg.Secret)) if err != nil { return nil, err } @@ -64,13 +66,35 @@ func NewClientManager(cliCfg *config.ProxyClient, cfg *config.UpStream) (*Client }, nil } -func (cliMgr *ClientManager) doLogin() *upClient { - loginCli := newUpClient(cliMgr.cfg, cliMgr) +func (cliMgr *ClientManager) doLogin(ctx context.Context, loginCli *upClient) time.Time { + // Calculate wait time between 2 seconds and cfg.RetryWaitTime + minWait := 2 * time.Second + waitDuration := time.Duration(cliMgr.cfg.RetryWaitTime) * time.Second + if waitDuration < minWait { + waitDuration = minWait + } - return loginCli + for { + log.Info("attempting login to upstream coordinator", "baseURL", cliMgr.cfg.BaseUrl) + loginResult, err := loginCli.Login(ctx) + if err == nil && loginResult != nil { + log.Info("login to upstream coordinator successful", "baseURL", cliMgr.cfg.BaseUrl, "time", loginResult.Time) + return loginResult.Time + } + log.Info("login to upstream coordinator failed, retrying", "baseURL", cliMgr.cfg.BaseUrl, "error", err, "waitDuration", waitDuration) + + timer := time.NewTimer(waitDuration) + select { + case <-ctx.Done(): + timer.Stop() + return time.Now() + case <-timer.C: + // Continue to next retry + } + } } -func (cliMgr *ClientManager) Client(ctx *gin.Context) *upClient { +func (cliMgr *ClientManager) Client(ctx context.Context) *upClient { cliMgr.cachedCli.RLock() if cliMgr.cachedCli.cli != nil { defer cliMgr.cachedCli.RUnlock() @@ -91,21 +115,54 @@ func (cliMgr *ClientManager) Client(ctx *gin.Context) *upClient { } else { // Set new completion context and launch login goroutine ctx, completionDone := context.WithCancel(context.TODO()) - cliMgr.cachedCli.completionCtx = ctx + loginCli := newUpClient(cliMgr.cfg, cliMgr) + cliMgr.cachedCli.completionCtx = context.WithValue(ctx, "cli", loginCli) // Launch login goroutine go func() { defer completionDone() + expiredT := cliMgr.doLogin(context.Background(), loginCli) + + cliMgr.cachedCli.Lock() + cliMgr.cachedCli.cli = loginCli + cliMgr.cachedCli.completionCtx = nil + + // Launch waiting thread to clear cached client before expiration + go func() { + now := time.Now() + clearTime := expiredT.Add(-10 * time.Second) // 10s before expiration + + // If clear time is too soon (less than 10s from now), set it to 10s from now + if clearTime.Before(now.Add(10 * time.Second)) { + clearTime = now.Add(10 * time.Second) + log.Error("token expiration time is too close, delaying clear time", + "baseURL", cliMgr.cfg.BaseUrl, + "expiredT", expiredT, + "adjustedClearTime", clearTime) + } + + waitDuration := time.Until(clearTime) + log.Info("token expiration monitor started", + "baseURL", cliMgr.cfg.BaseUrl, + "expiredT", expiredT, + "clearTime", clearTime, + "waitDuration", waitDuration) + + timer := time.NewTimer(waitDuration) + select { + case <-ctx.Done(): + timer.Stop() + log.Info("token expiration monitor cancelled", "baseURL", cliMgr.cfg.BaseUrl) + case <-timer.C: + log.Info("clearing cached client before token expiration", + "baseURL", cliMgr.cfg.BaseUrl, + "expiredT", expiredT) + cliMgr.clearCachedCli(loginCli) + } + }() + + cliMgr.cachedCli.Unlock() - loginCli := cliMgr.doLogin() - if loginResult, err := loginCli.Login(context.Background()); err == nil { - loginCli.loginToken = loginResult.Token - - cliMgr.cachedCli.Lock() - cliMgr.cachedCli.cli = loginCli - cliMgr.cachedCli.completionCtx = nil - cliMgr.cachedCli.Unlock() - } }() } cliMgr.cachedCli.Unlock() @@ -115,15 +172,26 @@ func (cliMgr *ClientManager) Client(ctx *gin.Context) *upClient { case <-ctx.Done(): return nil case <-completionCtx.Done(): - cliMgr.cachedCli.Lock() - cli := cliMgr.cachedCli.cli - cliMgr.cachedCli.Unlock() + cli := completionCtx.Value("cli").(*upClient) return cli } } -func (cliMgr *ClientManager) OnError(isUnauth bool) { +func (cliMgr *ClientManager) clearCachedCli(cli *upClient) { + cliMgr.cachedCli.Lock() + if cliMgr.cachedCli.cli == cli { + cliMgr.cachedCli.cli = nil + cliMgr.cachedCli.completionCtx = nil + log.Info("cached client cleared due to forbidden response", "baseURL", cliMgr.cfg.BaseUrl) + } + cliMgr.cachedCli.Unlock() +} +func (cliMgr *ClientManager) OnResp(cli *upClient, resp *http.Response) { + if resp.StatusCode == http.StatusForbidden { + log.Info("cached client cleared due to forbidden response", "baseURL", cliMgr.cfg.BaseUrl) + cliMgr.clearCachedCli(cli) + } } func (cliMgr *ClientManager) GenLoginParam(challenge string) (*types.LoginParameter, error) { From 321dd43af8c0d6e74734cbaf676b31d571bacc8b Mon Sep 17 00:00:00 2001 From: Ho Date: Mon, 25 Aug 2025 11:43:50 +0900 Subject: [PATCH 12/13] unit test for client --- coordinator/test/proxy_test.go | 69 ++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 coordinator/test/proxy_test.go diff --git a/coordinator/test/proxy_test.go b/coordinator/test/proxy_test.go new file mode 100644 index 0000000000..b287e41d8b --- /dev/null +++ b/coordinator/test/proxy_test.go @@ -0,0 +1,69 @@ +package test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "scroll-tech/coordinator/internal/config" + "scroll-tech/coordinator/internal/controller/proxy" +) + +func testProxyClientCfg() *config.ProxyClient { + + return &config.ProxyClient{ + Secret: "test-secret-key", + ProxyName: "test-proxy", + } +} + +func testProxyUpStreamCfg(coordinatorURL string) *config.UpStream { + + return &config.UpStream{ + BaseUrl: fmt.Sprintf("http://%s", coordinatorURL), + RetryWaitTime: 3, + ConnectionTimeoutSec: 30, + } + +} + +func testProxyClient(t *testing.T) { + + // Setup coordinator and http server. + coordinatorURL := randomURL() + proofCollector, httpHandler := setupCoordinator(t, 1, coordinatorURL) + defer func() { + proofCollector.Stop() + assert.NoError(t, httpHandler.Shutdown(context.Background())) + }() + + cliCfg := testProxyClientCfg() + upCfg := testProxyUpStreamCfg(coordinatorURL) + + clientManager, err := proxy.NewClientManager(cliCfg, upCfg) + assert.NoError(t, err) + assert.NotNil(t, clientManager) + + // Create context with timeout + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Test Client method + client := clientManager.Client(ctx) + + // Client should not be nil if login succeeds + // Note: This might be nil if the coordinator is not properly set up for proxy authentication + // but the test validates that the Client method completes without panic + t.Logf("Client toke: %v", client) + +} + +func TestProxyClient(t *testing.T) { + + // Set up the test environment. + setEnv(t) + t.Run("TestProxyHandshake", testProxyClient) +} From 64ef0f4ec038c1a8a41950a408bc3228cfde7a46 Mon Sep 17 00:00:00 2001 From: Ho Date: Mon, 25 Aug 2025 11:52:03 +0900 Subject: [PATCH 13/13] WIP --- .../controller/proxy/client_manager.go | 22 ++++++++++--------- .../internal/controller/proxy/controller.go | 2 +- coordinator/test/proxy_test.go | 2 +- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/coordinator/internal/controller/proxy/client_manager.go b/coordinator/internal/controller/proxy/client_manager.go index bae8954998..ec57a3b351 100644 --- a/coordinator/internal/controller/proxy/client_manager.go +++ b/coordinator/internal/controller/proxy/client_manager.go @@ -21,6 +21,7 @@ type Client interface { } type ClientManager struct { + name string cliCfg *config.ProxyClient cfg *config.UpStream privKey *ecdsa.PrivateKey @@ -52,7 +53,7 @@ func buildPrivateKey(inputBytes []byte) (*ecdsa.PrivateKey, error) { return nil, fmt.Errorf("failed to generate valid private key from input bytes") } -func NewClientManager(cliCfg *config.ProxyClient, cfg *config.UpStream) (*ClientManager, error) { +func NewClientManager(name string, cliCfg *config.ProxyClient, cfg *config.UpStream) (*ClientManager, error) { privKey, err := buildPrivateKey([]byte(cliCfg.Secret)) if err != nil { @@ -60,6 +61,7 @@ func NewClientManager(cliCfg *config.ProxyClient, cfg *config.UpStream) (*Client } return &ClientManager{ + name: name, privKey: privKey, cfg: cfg, cliCfg: cliCfg, @@ -75,13 +77,13 @@ func (cliMgr *ClientManager) doLogin(ctx context.Context, loginCli *upClient) ti } for { - log.Info("attempting login to upstream coordinator", "baseURL", cliMgr.cfg.BaseUrl) + log.Info("attempting login to upstream coordinator", "name", cliMgr.name) loginResult, err := loginCli.Login(ctx) if err == nil && loginResult != nil { - log.Info("login to upstream coordinator successful", "baseURL", cliMgr.cfg.BaseUrl, "time", loginResult.Time) + log.Info("login to upstream coordinator successful", "name", cliMgr.name, "time", loginResult.Time) return loginResult.Time } - log.Info("login to upstream coordinator failed, retrying", "baseURL", cliMgr.cfg.BaseUrl, "error", err, "waitDuration", waitDuration) + log.Info("login to upstream coordinator failed, retrying", "name", cliMgr.name, "error", err, "waitDuration", waitDuration) timer := time.NewTimer(waitDuration) select { @@ -136,14 +138,14 @@ func (cliMgr *ClientManager) Client(ctx context.Context) *upClient { if clearTime.Before(now.Add(10 * time.Second)) { clearTime = now.Add(10 * time.Second) log.Error("token expiration time is too close, delaying clear time", - "baseURL", cliMgr.cfg.BaseUrl, + "name", cliMgr.name, "expiredT", expiredT, "adjustedClearTime", clearTime) } waitDuration := time.Until(clearTime) log.Info("token expiration monitor started", - "baseURL", cliMgr.cfg.BaseUrl, + "name", cliMgr.name, "expiredT", expiredT, "clearTime", clearTime, "waitDuration", waitDuration) @@ -152,10 +154,10 @@ func (cliMgr *ClientManager) Client(ctx context.Context) *upClient { select { case <-ctx.Done(): timer.Stop() - log.Info("token expiration monitor cancelled", "baseURL", cliMgr.cfg.BaseUrl) + log.Info("token expiration monitor cancelled", "name", cliMgr.name) case <-timer.C: log.Info("clearing cached client before token expiration", - "baseURL", cliMgr.cfg.BaseUrl, + "name", cliMgr.name, "expiredT", expiredT) cliMgr.clearCachedCli(loginCli) } @@ -182,14 +184,14 @@ func (cliMgr *ClientManager) clearCachedCli(cli *upClient) { if cliMgr.cachedCli.cli == cli { cliMgr.cachedCli.cli = nil cliMgr.cachedCli.completionCtx = nil - log.Info("cached client cleared due to forbidden response", "baseURL", cliMgr.cfg.BaseUrl) + log.Info("cached client cleared due to forbidden response", "name", cliMgr.name) } cliMgr.cachedCli.Unlock() } func (cliMgr *ClientManager) OnResp(cli *upClient, resp *http.Response) { if resp.StatusCode == http.StatusForbidden { - log.Info("cached client cleared due to forbidden response", "baseURL", cliMgr.cfg.BaseUrl) + log.Info("cached client cleared due to forbidden response", "name", cliMgr.name) cliMgr.clearCachedCli(cli) } } diff --git a/coordinator/internal/controller/proxy/controller.go b/coordinator/internal/controller/proxy/controller.go index 297f8c68a4..5c42927685 100644 --- a/coordinator/internal/controller/proxy/controller.go +++ b/coordinator/internal/controller/proxy/controller.go @@ -35,7 +35,7 @@ func InitController(cfg *config.ProxyConfig) { clients := make(map[string]Client) for nm, upCfg := range cfg.Coordinators { - cli, err := NewClientManager(cfg.ProxyManager.Client, upCfg) + cli, err := NewClientManager(nm, cfg.ProxyManager.Client, upCfg) if err != nil { panic("create new client fail") } diff --git a/coordinator/test/proxy_test.go b/coordinator/test/proxy_test.go index b287e41d8b..670d6548ca 100644 --- a/coordinator/test/proxy_test.go +++ b/coordinator/test/proxy_test.go @@ -43,7 +43,7 @@ func testProxyClient(t *testing.T) { cliCfg := testProxyClientCfg() upCfg := testProxyUpStreamCfg(coordinatorURL) - clientManager, err := proxy.NewClientManager(cliCfg, upCfg) + clientManager, err := proxy.NewClientManager("test_coordinator", cliCfg, upCfg) assert.NoError(t, err) assert.NotNil(t, clientManager)