From 35c60466846cc461411682775f593527dc5dc25a Mon Sep 17 00:00:00 2001 From: naman <35531672+i-naman@users.noreply.github.com> Date: Tue, 30 Jun 2026 00:09:04 +0530 Subject: [PATCH 1/4] fix(ci): lowercase IMAGE_NAME for GHCR compatibility github.repository_owner returns JupiterMetaLabs (mixed case); Docker registry requires all-lowercase repository names. --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index f77a790..6fddb56 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -15,7 +15,7 @@ on: env: REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository_owner }}/jmdn + IMAGE_NAME: jupitermetalabs/jmdn jobs: build-and-push: From 33dc64aafa9151e137e275b998f425615d0dada9 Mon Sep 17 00:00:00 2001 From: naman <35531672+i-naman@users.noreply.github.com> Date: Tue, 30 Jun 2026 00:43:19 +0530 Subject: [PATCH 2/4] ci(docker): drop linux/arm/v7 from build matrix arm/v7 (Raspberry Pi 2/3, 32-bit ARM) is not a supported exchange node platform. Removing it to unblock the multi-arch manifest push for amd64 + arm64. --- .github/workflows/docker.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 6fddb56..4a94bd6 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -40,10 +40,6 @@ jobs: - platform: linux/arm64 runner_arch: arm64 - # ── linux/arm/v7 ───────────────────────────────────── - # Raspberry Pi 2/3, older 32-bit ARM SBCs - - platform: linux/arm/v7 - runner_arch: armv7 steps: - name: Checkout From 7391bf1e006c4dbaab50d4a9f0c6f18ac2c3219e Mon Sep 17 00:00:00 2001 From: Naman Agrawal <35531672+i-naman@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:58:00 +0530 Subject: [PATCH 3/4] fix(rpc): use account nonce for eth_getTransactionCount (#50) * fix(rpc): use account nonce for eth_getTransactionCount * fix: CI fmt and lint --- gETH/Facade/Service/Service.go | 27 ++++++++++++----- gETH/Facade/rpc/handlers.go | 55 ++++++++++++++++++++++++---------- gETH/Facade/rpc/http_server.go | 52 +++++++++++++++++++++++++++++++- gETH/Facade/rpc/ws_server.go | 2 +- 4 files changed, 112 insertions(+), 24 deletions(-) diff --git a/gETH/Facade/Service/Service.go b/gETH/Facade/Service/Service.go index 90ee385..93b33ce 100644 --- a/gETH/Facade/Service/Service.go +++ b/gETH/Facade/Service/Service.go @@ -87,20 +87,33 @@ func (s *ServiceImpl) BlockNumber(ctx context.Context) (*big.Int, error) { func (s *ServiceImpl) GetTransactionCount(ctx context.Context, addr string, block string) (*big.Int, error) { // Create a new context with timeout for this operation - _, cancel := context.WithTimeout(ctx, 10*time.Second) + opCtx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() - // Return the transaction count for the given address of latest block - Transactions, err := DB_OPs.CountTransactions(nil) + // Normalize the address (handles mixed-case / checksum addresses) + convertedAddr := Utils.ConvertAddressCaseInsensitive(addr) + + // TxNonce on the Account record is the authoritative Ethereum nonce. + // It is maintained by block processing as: account.TxNonce = tx.Nonce + 1 + // CountTransactionsByAccount is NOT used here because it counts both FROM and TO + // transactions, which would inflate the nonce for recipient addresses. + account, err := DB_OPs.GetAccount(nil, convertedAddr) if err != nil { + if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "does not exist") { + // Address has no transactions yet — nonce is 0 + return big.NewInt(0), nil + } + if logErr := Logger.LogData(opCtx, fmt.Sprintf("GetTransactionCount failed: %v", err), "GetTransactionCount", -1); logErr != nil { + fmt.Printf("Failed to log GetTransactionCount error: %v\n", logErr) + } return nil, err } - // fmt.Println("Transactions: ", Transactions) - // Convert the Transactions to big.Int - TransactionsBigInt := big.NewInt(int64(Transactions)) + if logErr := Logger.LogData(opCtx, fmt.Sprintf("GetTransactionCount returned nonce %d for %s", account.TxNonce, addr), "GetTransactionCount", 1); logErr != nil { + fmt.Printf("Failed to log GetTransactionCount: %v\n", logErr) + } - return TransactionsBigInt, nil + return big.NewInt(int64(account.TxNonce)), nil } func (s *ServiceImpl) BlockByNumber(ctx context.Context, num *big.Int, fullTx bool) (*Types.Block, error) { diff --git a/gETH/Facade/rpc/handlers.go b/gETH/Facade/rpc/handlers.go index 925df02..3589149 100644 --- a/gETH/Facade/rpc/handlers.go +++ b/gETH/Facade/rpc/handlers.go @@ -475,26 +475,51 @@ func toFilterQuery(p any) (*Types.FilterQuery, error) { return &Types.FilterQuery{}, errors.New("invalid filter object") } +// sha3UnclesEmpty is the Keccak-256 of an empty uncle list — standard constant for PoS/non-PoW chains. +const sha3UnclesEmpty = "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + func marshalBlock(b *Types.Block, full bool) map[string]any { result := map[string]any{ - "number": "0x" + new(big.Int).SetUint64(b.Header.Number).Text(16), - "hash": "0x" + hex.EncodeToString(b.Header.Hash), - "parentHash": "0x" + hex.EncodeToString(b.Header.ParentHash), - "timestamp": "0x" + new(big.Int).SetUint64(b.Header.Timestamp).Text(16), - "gasLimit": "0x" + new(big.Int).SetUint64(b.Header.GasLimit).Text(16), - "gasUsed": "0x" + new(big.Int).SetUint64(b.Header.GasUsed).Text(16), + // Core identity + "number": "0x" + new(big.Int).SetUint64(b.Header.Number).Text(16), + "hash": "0x" + hex.EncodeToString(b.Header.Hash), + "parentHash": "0x" + hex.EncodeToString(b.Header.ParentHash), + "timestamp": "0x" + new(big.Int).SetUint64(b.Header.Timestamp).Text(16), + + // Gas + "gasLimit": "0x" + new(big.Int).SetUint64(b.Header.GasLimit).Text(16), + "gasUsed": "0x" + new(big.Int).SetUint64(b.Header.GasUsed).Text(16), + + // State / receipts — all stored in ZKBlock, surfaced via BlockHeader + "stateRoot": "0x" + hex.EncodeToString(b.Header.StateRoot), + "receiptsRoot": "0x" + hex.EncodeToString(b.Header.ReceiptsRoot), + "logsBloom": "0x" + hex.EncodeToString(b.Header.LogsBloom), + "extraData": "0x" + hex.EncodeToString(b.Header.ExtraData), + + // Miner / sequencer address (ZKVMAddr in ZKBlock → Miner in BlockHeader) + "miner": "0x" + hex.EncodeToString(b.Header.Miner), + + // EIP-1559 base fee — computed at read time (35 gwei constant) in ConvertZKBlockToblockheader. + // Guard against nil/empty for blocks written before this field existed. + "baseFeePerGas": func() string { + if len(b.Header.BaseFee) > 0 { + return "0x" + new(big.Int).SetBytes(b.Header.BaseFee).Text(16) + } + return "0x" + big.NewInt(35000000000).Text(16) // fallback: 35 gwei + }(), + + // PoW fields — this chain has no PoW; use standard empty/zero values + // so EIP-3675 (PoS) compatible clients don't reject the block envelope + "sha3Uncles": sha3UnclesEmpty, + "nonce": "0x0000000000000000", + "difficulty": "0x0", + "totalDifficulty": "0x0", + "mixHash": "0x" + strings.Repeat("0", 64), + "uncles": []string{}, + "transactions": []any{}, } - // Add baseFeePerGas at top-level from header (EIP-1559) - // if b.Header != nil && len(b.Header.BaseFee) > 0 { - // baseFeeBig := new(big.Int).SetBytes(b.Header.BaseFee) - // result["baseFeePerGas"] = "0x" + baseFeeBig.Text(16) - // } else { - // // If no base fee (pre-EIP-1559 blocks), set to null or omit - // result["baseFeePerGas"] = nil - // } - if full && len(b.Transactions) > 0 { txs := make([]any, len(b.Transactions)) for i, tx := range b.Transactions { diff --git a/gETH/Facade/rpc/http_server.go b/gETH/Facade/rpc/http_server.go index 003f4b4..7f7376a 100644 --- a/gETH/Facade/rpc/http_server.go +++ b/gETH/Facade/rpc/http_server.go @@ -2,9 +2,11 @@ package rpc import ( "context" + "encoding/json" "errors" "fmt" "net/http" + "sync" "time" "github.com/gin-gonic/gin" @@ -94,9 +96,57 @@ func (s *HTTPServer) ServeWithContext(ctx context.Context, addr string) error { } } +const maxBatchSize = 100 + func (s *HTTPServer) handleJSONRPC(c *gin.Context) { + body, err := c.GetRawData() + if err != nil || len(body) == 0 { + write(c, RespErr(nil, -32700, "Parse error")) + return + } + + // Detect batch vs single by inspecting first non-whitespace byte + isBatch := false + for _, b := range body { + if b == ' ' || b == '\t' || b == '\n' || b == '\r' { + continue + } + isBatch = b == '[' + break + } + + if isBatch { + var reqs []Request + if err := json.Unmarshal(body, &reqs); err != nil { + write(c, RespErr(nil, -32700, "Parse error")) + return + } + if len(reqs) == 0 { + write(c, RespErr(nil, -32600, "Invalid Request: empty batch")) + return + } + if len(reqs) > maxBatchSize { + write(c, RespErr(nil, -32600, fmt.Sprintf("batch too large: max %d requests", maxBatchSize))) + return + } + resps := make([]Response, len(reqs)) + var wg sync.WaitGroup + for i, req := range reqs { + wg.Add(1) + go func(i int, req Request) { + defer wg.Done() + resps[i], _ = s.h.Handle(c.Request.Context(), req) + }(i, req) + } + wg.Wait() + c.Header("Content-Type", "application/json") + c.JSON(http.StatusOK, resps) + return + } + + // Single request — identical to original behaviour var req Request - if err := c.ShouldBindJSON(&req); err != nil { + if err := json.Unmarshal(body, &req); err != nil { write(c, RespErr(nil, -32700, "Parse error")) return } diff --git a/gETH/Facade/rpc/ws_server.go b/gETH/Facade/rpc/ws_server.go index 2a5aa48..8d8edf6 100644 --- a/gETH/Facade/rpc/ws_server.go +++ b/gETH/Facade/rpc/ws_server.go @@ -229,7 +229,7 @@ func forwardBlocks(conn *websocket.Conn, sid string, ch <-chan *Types.Block) { } func forwardLogs(conn *websocket.Conn, sid string, ch <-chan Types.Log) { for l := range ch { - msg := subMsg{Jsonrpc: "2.o", Method: "eth_subscription"} + msg := subMsg{Jsonrpc: "2.0", Method: "eth_subscription"} msg.Params.Subscription = sid msg.Params.Result = marshalLogs([]Types.Log{l})[0] _ = conn.WriteJSON(msg) From cb02a7fe0d09bfc767ef804355b797ad3514f55a Mon Sep 17 00:00:00 2001 From: Naman Agrawal <35531672+i-naman@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:23:57 +0530 Subject: [PATCH 4/4] fix(gatekeeper): make mTLS client cert optional, warn instead of fatal (#51) --- pkg/gatekeeper/tls.go | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/pkg/gatekeeper/tls.go b/pkg/gatekeeper/tls.go index 9f61b2f..f46ffe8 100644 --- a/pkg/gatekeeper/tls.go +++ b/pkg/gatekeeper/tls.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "crypto/x509" + "errors" "fmt" "os" @@ -164,15 +165,36 @@ func (l *TLSLoader) LoadClientTLS(targetServiceName string, clientIdentity strin } // 3. Load client certificate for mTLS when an identity is provided. + // + // The client cert is OPTIONAL: if the cert file is absent, we proceed with + // one-way TLS (server-auth only). This is the correct behaviour for public + // endpoints like mre.jmdt.io whose nginx terminates TLS without requiring a + // client certificate. A hard failure here would leave the routing client + // singleton nil and silently block all transaction submissions. + // + // We only hard-fail if the cert file EXISTS but cannot be read/parsed, which + // indicates a genuine provisioning error rather than a deliberate omission. if clientIdentity != "" { certFile := fmt.Sprintf("%s/%s.crt", l.config.CertDir, clientIdentity) keyFile := fmt.Sprintf("%s/%s.key", l.config.CertDir, clientIdentity) - cert, err := tls.LoadX509KeyPair(certFile, keyFile) - if err != nil { + switch cert, err := tls.LoadX509KeyPair(certFile, keyFile); { + case err == nil: + // mTLS client cert found and loaded — present it to the server. + tlsConfig.Certificates = []tls.Certificate{cert} + + case errors.Is(err, os.ErrNotExist): + // Cert not provisioned on this node — proceed with one-way TLS. + // This is expected for nodes connecting to internet-facing endpoints. + l.logger.Warn(context.Background(), "mTLS client cert not found — proceeding with one-way TLS", + ion.String("identity", clientIdentity), + ion.String("cert_file", certFile), + ) + + default: + // File exists but failed to read or parse — genuine provisioning error. return nil, fmt.Errorf("gatekeeper: failed to load client identity %s: %w", clientIdentity, err) } - tlsConfig.Certificates = []tls.Certificate{cert} } return tlsConfig, nil