diff --git a/example_env b/example_env new file mode 100644 index 00000000..858b0be5 --- /dev/null +++ b/example_env @@ -0,0 +1,5 @@ +# Options: debug, info, warn, error, fatal +# Default: info +LOG_LEVEL=info +LOG_TRACING=1 + diff --git a/go.mod b/go.mod index 0ae6962d..5ac7fa93 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( github.com/stretchr/testify v1.10.0 go.uber.org/mock v0.5.2 go.uber.org/ratelimit v0.3.1 + go.uber.org/zap v1.27.0 golang.org/x/crypto v0.36.0 golang.org/x/sync v0.12.0 golang.org/x/sys v0.31.0 diff --git a/go.sum b/go.sum index bea6211f..1d335c9a 100644 --- a/go.sum +++ b/go.sum @@ -833,6 +833,8 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -846,6 +848,8 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/p2p/kademlia/bootstrap.go b/p2p/kademlia/bootstrap.go index 8ce18f10..8685850a 100644 --- a/p2p/kademlia/bootstrap.go +++ b/p2p/kademlia/bootstrap.go @@ -192,7 +192,11 @@ func (s *DHT) ConfigureBootstrapNodes(ctx context.Context, bootstrapNodes string for _, node := range mapNodes { hID, _ := utils.Blake3Hash(node.ID) node.HashedID = hID - fmt.Println("node adding", node.String(), "hashed id", string(node.HashedID)) + logtrace.Debug(ctx, "node adding", logtrace.Fields{ + logtrace.FieldModule: "p2p", + "node": node.String(), + "hashed_id": string(node.HashedID), + }) boostrapNodes = append(boostrapNodes, node) } } @@ -267,7 +271,7 @@ func (s *DHT) Bootstrap(ctx context.Context, bootstrapNodes string) error { // So if bootstrap failed, should try to connect to node again for next bootstrap retry // s.cache.SetWithExpiry(addr, []byte("true"), badAddrExpiryHours*time.Hour) - logtrace.Error(ctx, "network call failed, sleeping 3 seconds", logtrace.Fields{ + logtrace.Debug(ctx, "network call failed, sleeping 3 seconds", logtrace.Fields{ logtrace.FieldModule: "p2p", logtrace.FieldError: err.Error(), }) diff --git a/p2p/kademlia/network.go b/p2p/kademlia/network.go index 88100057..107f7421 100644 --- a/p2p/kademlia/network.go +++ b/p2p/kademlia/network.go @@ -348,7 +348,7 @@ func (s *Network) handleConn(ctx context.Context, rawConn net.Conn) { conn, err = NewSecureServerConn(ctx, s.tc, rawConn) if err != nil { rawConn.Close() - logtrace.Error(ctx, "Server secure handshake failed", logtrace.Fields{ + logtrace.Warn(ctx, "Server secure handshake failed", logtrace.Fields{ logtrace.FieldModule: "p2p", logtrace.FieldError: err.Error(), }) @@ -373,7 +373,7 @@ func (s *Network) handleConn(ctx context.Context, rawConn net.Conn) { if err == io.EOF { return } - logtrace.Error(ctx, "Read and decode failed", logtrace.Fields{ + logtrace.Warn(ctx, "Read and decode failed", logtrace.Fields{ logtrace.FieldModule: "p2p", logtrace.FieldError: err.Error(), }) diff --git a/pkg/logtrace/grpc_logger.go b/pkg/logtrace/grpc_logger.go index 5d444b5f..28b5cdcb 100644 --- a/pkg/logtrace/grpc_logger.go +++ b/pkg/logtrace/grpc_logger.go @@ -3,81 +3,80 @@ package logtrace import ( "context" "fmt" + "os" "google.golang.org/grpc/grpclog" ) // grpcLogger implements grpclog.LoggerV2 interface using logtrace -type grpcLogger struct { - ctx context.Context -} +type grpcLogger struct{} // NewGRPCLogger creates a new gRPC-compatible logger using logtrace -func NewGRPCLogger(ctx context.Context) grpclog.LoggerV2 { - return &grpcLogger{ctx: ctx} +func NewGRPCLogger() grpclog.LoggerV2 { + return &grpcLogger{} } // Info logs at info level -func (g *grpcLogger) Info(args ...interface{}) { - Info(g.ctx, fmt.Sprint(args...), Fields{FieldModule: "grpc"}) +func (g *grpcLogger) Info(args ...any) { + Debug(context.Background(), fmt.Sprint(args...), Fields{"module": "grpc"}) // Suppress Internal Logs } // Infof logs at info level with format -func (g *grpcLogger) Infof(format string, args ...interface{}) { - Info(g.ctx, fmt.Sprintf(format, args...), Fields{FieldModule: "grpc"}) +func (g *grpcLogger) Infof(format string, args ...any) { + Debug(context.Background(), fmt.Sprintf(format, args...), Fields{"module": "grpc"}) } // Infoln logs at info level with newline -func (g *grpcLogger) Infoln(args ...interface{}) { +func (g *grpcLogger) Infoln(args ...any) { g.Info(args...) } // Warning logs at warn level -func (g *grpcLogger) Warning(args ...interface{}) { - Warn(g.ctx, fmt.Sprint(args...), Fields{FieldModule: "grpc"}) +func (g *grpcLogger) Warning(args ...any) { + Warn(context.Background(), fmt.Sprint(args...), Fields{"module": "grpc"}) } // Warningf logs at warn level with format -func (g *grpcLogger) Warningf(format string, args ...interface{}) { - Warn(g.ctx, fmt.Sprintf(format, args...), Fields{FieldModule: "grpc"}) +func (g *grpcLogger) Warningf(format string, args ...any) { + Warn(context.Background(), fmt.Sprintf(format, args...), Fields{"module": "grpc"}) } // Warningln logs at warn level with newline -func (g *grpcLogger) Warningln(args ...interface{}) { +func (g *grpcLogger) Warningln(args ...any) { g.Warning(args...) } // Error logs at error level -func (g *grpcLogger) Error(args ...interface{}) { - Error(g.ctx, fmt.Sprint(args...), Fields{FieldModule: "grpc"}) +func (g *grpcLogger) Error(args ...any) { + Error(context.Background(), fmt.Sprint(args...), Fields{"module": "grpc"}) } // Errorf logs at error level with format -func (g *grpcLogger) Errorf(format string, args ...interface{}) { - Error(g.ctx, fmt.Sprintf(format, args...), Fields{FieldModule: "grpc"}) +func (g *grpcLogger) Errorf(format string, args ...any) { + Error(context.Background(), fmt.Sprintf(format, args...), Fields{"module": "grpc"}) } // Errorln logs at error level with newline -func (g *grpcLogger) Errorln(args ...interface{}) { +func (g *grpcLogger) Errorln(args ...any) { g.Error(args...) } -// Fatal logs at error level and panics -func (g *grpcLogger) Fatal(args ...interface{}) { +// Fatal logs at error level and exits gracefully +func (g *grpcLogger) Fatal(args ...any) { msg := fmt.Sprint(args...) - Error(g.ctx, msg, Fields{FieldModule: "grpc", "level": "fatal"}) - panic(msg) + Error(context.Background(), msg, Fields{"module": "grpc", "level": "fatal"}) + os.Exit(1) } -// Fatalf logs at error level with format and panics -func (g *grpcLogger) Fatalf(format string, args ...interface{}) { +// Fatalf logs at error level with format and exits gracefully +func (g *grpcLogger) Fatalf(format string, args ...any) { msg := fmt.Sprintf(format, args...) - Error(g.ctx, msg, Fields{FieldModule: "grpc", "level": "fatal"}) - panic(msg) + Error(context.Background(), msg, Fields{"module": "grpc", "level": "fatal"}) + os.Exit(1) } -// Fatalln logs at error level with newline and panics -func (g *grpcLogger) Fatalln(args ...interface{}) { +// Fatalln logs at error level with newline and exits +func (g *grpcLogger) Fatalln(args ...any) { g.Fatal(args...) } @@ -87,6 +86,6 @@ func (g *grpcLogger) V(l int) bool { } // SetGRPCLogger configures gRPC to use logtrace for internal logging -func SetGRPCLogger(ctx context.Context) { - grpclog.SetLoggerV2(NewGRPCLogger(ctx)) +func SetGRPCLogger() { + grpclog.SetLoggerV2(NewGRPCLogger()) } diff --git a/pkg/logtrace/log.go b/pkg/logtrace/log.go index 8cfa77a6..90846308 100644 --- a/pkg/logtrace/log.go +++ b/pkg/logtrace/log.go @@ -2,14 +2,13 @@ package logtrace import ( "context" - "fmt" - "log/slog" - "maps" "os" "runtime" -) + "strings" -type LogLevel func(msg string, keysAndValues ...interface{}) + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) // ContextKey is the type used for storing values in context type ContextKey string @@ -17,54 +16,63 @@ type ContextKey string // CorrelationIDKey is the key for storing correlation ID in context const CorrelationIDKey ContextKey = "correlation_id" -// Setup initializes the logger with a specified log level -func Setup(serviceName, env string, level string) { - var slogLevel slog.Level - switch level { - case "warn": - slogLevel = slog.LevelWarn - case "error": - slogLevel = slog.LevelError - case "debug": - slogLevel = slog.LevelDebug - default: - slogLevel = slog.LevelInfo - } +var logger *zap.Logger - opts := &slog.HandlerOptions{ - AddSource: false, - Level: slogLevel, - } +// Setup initializes the logger for readable output in all modes. +func Setup(serviceName string) { + var err error - hostname, _ := os.Hostname() + // Always start with the development config for colored, readable logs. + config := zap.NewDevelopmentConfig() - logger := slog.New(slog.NewJSONHandler(os.Stdout, opts)) - logger = logger.With("hostname", hostname).With("service", fmt.Sprintf("%s-%s", serviceName, env)) - slog.SetDefault(logger) -} + // Ensure the log level is always colorized. + config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder -// log logs a message with additional fields using the specified log function. -func log(logLevel slog.Level, logFunc LogLevel, message string, fields Fields) { - fieldArgs := make([]interface{}, 0, len(fields)*2+1) + tracingEnabled := getTracingEnabled() + config.EncoderConfig.TimeKey = "" // Remove the timestamp + config.DisableCaller = true - // Only attach source info for TRACE/DEBUG level - if logLevel == slog.LevelDebug || logLevel == slog.LevelError { - if pc, file, line, ok := runtime.Caller(2); ok { - details := runtime.FuncForPC(pc) - fieldArgs = append(fieldArgs, slog.Group( - "source", - slog.String("filename", file), - slog.Int("lineno", line), - slog.String("function", details.Name()), - )) - } + // Always respect the LOG_LEVEL environment variable. + config.Level = zap.NewAtomicLevelAt(getLogLevel()) + + // Build the logger from the customized config. + if tracingEnabled { + logger, err = config.Build(zap.AddCallerSkip(1), zap.AddStacktrace(zapcore.ErrorLevel)) + } else { + logger, err = config.Build() } + if err != nil { + panic(err) + } +} - for key, value := range fields { - fieldArgs = append(fieldArgs, slog.Any(key, value)) +// getLogLevel returns the log level from environment variable LOG_LEVEL +func getLogLevel() zapcore.Level { + levelStr := strings.ToLower(os.Getenv("LOG_LEVEL")) + switch levelStr { + case "debug": + return zapcore.DebugLevel + case "info": + return zapcore.InfoLevel + case "warn", "warning": + return zapcore.WarnLevel + case "error": + return zapcore.ErrorLevel + case "fatal": + return zapcore.FatalLevel + default: + return zapcore.InfoLevel } +} - logFunc(message, fieldArgs...) +// getTracingEnabled returns whether tracing is enabled from LOG_TRACING env var +func getTracingEnabled() bool { + return strings.ToLower(os.Getenv("LOG_TRACING")) == "1" +} + +// CtxWithCorrelationID stores a correlation ID inside the context +func CtxWithCorrelationID(ctx context.Context, correlationID string) context.Context { + return context.WithValue(ctx, CorrelationIDKey, correlationID) } // extractCorrelationID retrieves the correlation ID from context @@ -75,35 +83,69 @@ func extractCorrelationID(ctx context.Context) string { return "unknown" } -// addCorrelationID ensures logs include the correlation ID without modifying the input fields. -func addCorrelationID(ctx context.Context, fields Fields) Fields { - newFields := make(Fields, len(fields)+1) // Copy fields to avoid mutation - maps.Copy(newFields, fields) - newFields[FieldCorrelationID] = extractCorrelationID(ctx) - return newFields +// logWithLevel logs a message with structured fields. +func logWithLevel(level zapcore.Level, ctx context.Context, message string, fields Fields) { + if logger == nil { + Setup("unknown-service") // Fallback if Setup wasn't called + } + + // Always enrich logs with the correlation ID. + // allFields := make(Fields, len(fields)+1) + // for k, v := range fields { + // allFields[k] = v + // } + // allFields[FieldCorrelationID] = extractCorrelationID(ctx) + + // Convert the map to a slice of zap.Field + zapFields := make([]zap.Field, 0, len(fields)) + for k, v := range fields { + zapFields = append(zapFields, zap.Any(k, v)) + } + + // Add stack trace if tracing is enabled + if getTracingEnabled() { + // Get caller information + if pc, file, line, ok := runtime.Caller(2); ok { + fn := runtime.FuncForPC(pc) + if fn != nil { + zapFields = append(zapFields, zap.String("caller", fn.Name())) + zapFields = append(zapFields, zap.String("file", file)) + zapFields = append(zapFields, zap.Int("line", line)) + } + } + } + + // Log with the structured fields. + switch level { + case zapcore.DebugLevel: + logger.Debug(message, zapFields...) + case zapcore.InfoLevel: + logger.Info(message, zapFields...) + case zapcore.WarnLevel: + logger.Warn(message, zapFields...) + case zapcore.ErrorLevel: + logger.Error(message, zapFields...) + case zapcore.FatalLevel: + logger.Fatal(message, zapFields...) + } } -// Error logs an error message with structured fields. +// Error logs an error message with structured fields func Error(ctx context.Context, message string, fields Fields) { - log(slog.LevelError, slog.Error, message, addCorrelationID(ctx, fields)) + logWithLevel(zapcore.ErrorLevel, ctx, message, fields) } -// Info logs an informational message with structured fields. +// Info logs an informational message with structured fields func Info(ctx context.Context, message string, fields Fields) { - log(slog.LevelInfo, slog.Info, message, addCorrelationID(ctx, fields)) + logWithLevel(zapcore.InfoLevel, ctx, message, fields) } -// Warn logs a warning message with structured fields. +// Warn logs a warning message with structured fields func Warn(ctx context.Context, message string, fields Fields) { - log(slog.LevelWarn, slog.Warn, message, addCorrelationID(ctx, fields)) + logWithLevel(zapcore.WarnLevel, ctx, message, fields) } -// Debug logs a debug message with structured fields. +// Debug logs a debug message with structured fields func Debug(ctx context.Context, message string, fields Fields) { - log(slog.LevelDebug, slog.Debug, message, addCorrelationID(ctx, fields)) -} - -// CtxWithCorrelationID stores a correlation ID inside the context. -func CtxWithCorrelationID(ctx context.Context, correlationID string) context.Context { - return context.WithValue(ctx, CorrelationIDKey, correlationID) + logWithLevel(zapcore.DebugLevel, ctx, message, fields) } diff --git a/pkg/lumera/modules/tx/impl.go b/pkg/lumera/modules/tx/impl.go index 4112ac2d..97ee0493 100644 --- a/pkg/lumera/modules/tx/impl.go +++ b/pkg/lumera/modules/tx/impl.go @@ -24,9 +24,6 @@ const ( DefaultGasPadding = uint64(50000) DefaultFeeDenom = "ulume" DefaultGasPrice = "0.000001" - - // Gas costs from chain parameters - TxSizeCostPerByte = uint64(10) // tx_size_cost_per_byte: "10" ) // module implements the Module interface @@ -45,107 +42,64 @@ func newModule(conn *grpc.ClientConn) (Module, error) { }, nil } -// calculateGasForTxSize calculates gas needed for transaction size -func (m *module) calculateGasForTxSize(txSizeBytes int) uint64 { - return uint64(txSizeBytes) * TxSizeCostPerByte -} - // SimulateTransaction simulates a transaction with given messages and returns gas used func (m *module) SimulateTransaction(ctx context.Context, msgs []types.Msg, accountInfo *authtypes.BaseAccount, config *TxConfig) (*sdktx.SimulateResponse, error) { - // Create encoding config + // Create encoding config and client context encCfg := lumeracodec.GetEncodingConfig() - - // Create client context clientCtx := client.Context{}. WithCodec(encCfg.Codec). WithTxConfig(encCfg.TxConfig). - WithKeyring(config.Keyring). - WithBroadcastMode("sync") - - // Get the key for public key - key, err := config.Keyring.Key(config.KeyName) - if err != nil { - return nil, fmt.Errorf("failed to get key from keyring: %w", err) - } - - pubKey, err := key.GetPubKey() - if err != nil { - return nil, fmt.Errorf("failed to get public key: %w", err) - } + WithKeyring(config.Keyring) - minFee := fmt.Sprintf("1%s", config.FeeDenom) - - // Build transaction with minimal gas to get size - txBuilder, err := tx.Factory{}. + // Create a transaction factory with Gas set to 0 to trigger auto-estimation + // and add a minimal fee to pass the mempool check during simulation. + txf := tx.Factory{}. WithTxConfig(clientCtx.TxConfig). WithKeybase(config.Keyring). WithAccountNumber(accountInfo.AccountNumber). WithSequence(accountInfo.Sequence). WithChainID(config.ChainID). - WithGas(10000). - WithGasAdjustment(config.GasAdjustment). + WithGas(0). // Setting Gas to 0 is the key for estimation WithSignMode(signingtypes.SignMode_SIGN_MODE_DIRECT). - WithFees(minFee). - BuildUnsignedTx(msgs...) + WithFees(fmt.Sprintf("1%s", config.FeeDenom)) // Minimal fee for simulation + + // Build the unsigned transaction once + txb, err := txf.BuildUnsignedTx(msgs...) if err != nil { - return nil, fmt.Errorf("failed to build unsigned tx: %w", err) + return nil, fmt.Errorf("failed to build unsigned tx for simulation: %w", err) } - // Set empty signature - txBuilder.SetSignatures(signingtypes.SignatureV2{ - PubKey: pubKey, - Data: &signingtypes.SingleSignatureData{SignMode: signingtypes.SignMode_SIGN_MODE_DIRECT, Signature: nil}, - Sequence: accountInfo.Sequence, - }) - - // Get transaction size - txBytes, err := clientCtx.TxConfig.TxEncoder()(txBuilder.GetTx()) + // Create a dummy signature to account for its size in the gas estimation + key, err := config.Keyring.Key(config.KeyName) if err != nil { - return nil, fmt.Errorf("failed to encode transaction: %w", err) + return nil, fmt.Errorf("failed to get key from keyring: %w", err) } - - // Calculate required gas and rebuild - gasLimit := m.calculateGasForTxSize(len(txBytes)) - - txBuilder, err = tx.Factory{}. - WithTxConfig(clientCtx.TxConfig). - WithKeybase(config.Keyring). - WithAccountNumber(accountInfo.AccountNumber). - WithSequence(accountInfo.Sequence). - WithChainID(config.ChainID). - WithGas(gasLimit). - WithGasAdjustment(config.GasAdjustment). - WithSignMode(signingtypes.SignMode_SIGN_MODE_DIRECT). - WithFees(minFee). - BuildUnsignedTx(msgs...) + pubKey, err := key.GetPubKey() if err != nil { - return nil, fmt.Errorf("failed to rebuild tx: %w", err) + return nil, fmt.Errorf("failed to get public key: %w", err) } - - // Reset signature - txBuilder.SetSignatures(signingtypes.SignatureV2{ + sig := signingtypes.SignatureV2{ PubKey: pubKey, - Data: &signingtypes.SingleSignatureData{SignMode: signingtypes.SignMode_SIGN_MODE_DIRECT, Signature: nil}, + Data: &signingtypes.SingleSignatureData{SignMode: txf.SignMode(), Signature: nil}, Sequence: accountInfo.Sequence, - }) + } + if err := txb.SetSignatures(sig); err != nil { + return nil, fmt.Errorf("failed to set dummy signature: %w", err) + } - // Encode final transaction - txBytes, err = clientCtx.TxConfig.TxEncoder()(txBuilder.GetTx()) + // Encode the transaction for simulation + txBytes, err := clientCtx.TxConfig.TxEncoder()(txb.GetTx()) if err != nil { - return nil, fmt.Errorf("failed to encode transaction: %w", err) + return nil, fmt.Errorf("failed to encode transaction for simulation: %w", err) } - logtrace.Info(ctx, fmt.Sprintf("simulating transaction | txSize=%d gasLimit=%d", len(txBytes), gasLimit), nil) - - // Simulate transaction + // Simulate the transaction simRes, err := m.client.Simulate(ctx, &sdktx.SimulateRequest{TxBytes: txBytes}) if err != nil { - logtrace.Error(ctx, fmt.Sprintf("simulation failed | error=%s txSize=%d gasLimit=%d", err.Error(), len(txBytes), gasLimit), nil) return nil, fmt.Errorf("simulation error: %w", err) } - logtrace.Info(ctx, fmt.Sprintf("simulation complete | gasUsed=%d gasWanted=%d", simRes.GasInfo.GasUsed, simRes.GasInfo.GasWanted), nil) - + logtrace.Info(ctx, fmt.Sprintf("simulation complete | gasUsed=%d", simRes.GasInfo.GasUsed), nil) return simRes, nil } @@ -185,7 +139,7 @@ func (m *module) BuildAndSignTransaction(ctx context.Context, msgs []types.Msg, return nil, fmt.Errorf("failed to sign transaction: %w", err) } - logtrace.Info(ctx, fmt.Sprintf("transaction signed successfully"), nil) + logtrace.Info(ctx, "transaction signed successfully", nil) // Encode signed transaction txBytes, err := clientCtx.TxConfig.TxEncoder()(txBuilder.GetTx()) @@ -230,13 +184,13 @@ func (m *module) CalculateFee(gasAmount uint64, config *TxConfig) string { // ProcessTransaction handles the complete flow: simulate, build, sign, and broadcast func (m *module) ProcessTransaction(ctx context.Context, msgs []types.Msg, accountInfo *authtypes.BaseAccount, config *TxConfig) (*sdktx.BroadcastTxResponse, error) { // Step 1: Simulate transaction to get gas estimate - simulatedGas, err := m.SimulateTransaction(ctx, msgs, accountInfo, config) + simRes, err := m.SimulateTransaction(ctx, msgs, accountInfo, config) if err != nil { return nil, fmt.Errorf("simulation failed: %w", err) } // Step 2: Calculate gas with adjustment and padding - simulatedGasUsed := simulatedGas.GasInfo.GasUsed + simulatedGasUsed := simRes.GasInfo.GasUsed adjustedGas := uint64(float64(simulatedGasUsed) * config.GasAdjustment) gasToUse := adjustedGas + config.GasPadding diff --git a/pkg/net/grpc/client/client.go b/pkg/net/grpc/client/client.go index be8ffbe7..37862f18 100644 --- a/pkg/net/grpc/client/client.go +++ b/pkg/net/grpc/client/client.go @@ -315,10 +315,9 @@ func (ch *defaultConnectionHandler) retryConnection(ctx context.Context, address return nil, fmt.Errorf("connection failed after %d attempts: %w", opts.MaxRetries, lastErr) } -// configureContext ensures the context has a timeout and sets up logging // configureContext ensures the context has a timeout and sets up logging func (ch *defaultConnectionHandler) configureContext(ctx context.Context) (context.Context, context.CancelFunc) { - logtrace.SetGRPCLogger(ctx) + logtrace.SetGRPCLogger() id, _ := random.String(8, random.Base62Chars) ctx = logtrace.CtxWithCorrelationID(ctx, fmt.Sprintf("%s-%s", logPrefix, id)) diff --git a/pkg/net/grpc/server/server.go b/pkg/net/grpc/server/server.go index 4eca05d4..55d946a2 100644 --- a/pkg/net/grpc/server/server.go +++ b/pkg/net/grpc/server/server.go @@ -217,7 +217,7 @@ func (s *Server) Serve(ctx context.Context, address string, opts *ServerOptions) opts = DefaultServerOptions() } - logtrace.SetGRPCLogger(ctx) + logtrace.SetGRPCLogger() ctx = logtrace.CtxWithCorrelationID(ctx, s.name) // Create server with options diff --git a/sdk/task/download.go b/sdk/task/download.go index 147ea9f0..62caa145 100644 --- a/sdk/task/download.go +++ b/sdk/task/download.go @@ -3,6 +3,7 @@ package task import ( "context" "fmt" + "os" "time" "github.com/LumeraProtocol/supernode/sdk/adapters/lumera" @@ -155,6 +156,13 @@ func (t *CascadeDownloadTask) attemptConcurrentDownload( req *supernodeservice.CascadeSupernodeDownloadRequest, baseIteration int, ) (*downloadResult, []error) { + // Remove existing file if it exists to allow overwrite (do this once before concurrent attempts) + if _, err := os.Stat(req.OutputPath); err == nil { + if removeErr := os.Remove(req.OutputPath); removeErr != nil { + return nil, []error{fmt.Errorf("failed to remove existing file %s: %w", req.OutputPath, removeErr)} + } + } + // Create a cancellable context for this batch batchCtx, cancelBatch := context.WithCancel(ctx) defer cancelBatch() @@ -208,7 +216,7 @@ func (t *CascadeDownloadTask) attemptConcurrentDownload( // Collect results var errors []error - for i := 0; i < len(batch); i++ { + for i := range len(batch) { select { case result := <-resultCh: if result.success != nil { diff --git a/supernode/cmd/start.go b/supernode/cmd/start.go index 6f01fc67..de846824 100644 --- a/supernode/cmd/start.go +++ b/supernode/cmd/start.go @@ -34,7 +34,7 @@ var startCmd = &cobra.Command{ The supernode will connect to the Lumera network and begin participating in the network.`, RunE: func(cmd *cobra.Command, args []string) error { // Initialize logging - logtrace.Setup("supernode", "dev", appConfig.LogConfig.Level) + logtrace.Setup("supernode") // Create context with correlation ID for tracing ctx := logtrace.CtxWithCorrelationID(context.Background(), "supernode-start") diff --git a/supernode/config.yml b/supernode/config.yml index 9e3fc579..90c7bed4 100644 --- a/supernode/config.yml +++ b/supernode/config.yml @@ -27,7 +27,3 @@ lumera: # RaptorQ Configuration raptorq: files_dir: "raptorq_files" - -#Logging Configuration -log: - level: "info" #debug, info, warn, error diff --git a/supernode/config/config.go b/supernode/config/config.go index 0c67f14c..69fc2d0f 100644 --- a/supernode/config/config.go +++ b/supernode/config/config.go @@ -49,7 +49,6 @@ type Config struct { P2PConfig `yaml:"p2p"` LumeraClientConfig `yaml:"lumera"` RaptorQConfig `yaml:"raptorq"` - LogConfig `yaml:"log"` // Store base directory (not from YAML) BaseDir string `yaml:"-"` @@ -145,7 +144,6 @@ func LoadConfig(filename string, baseDir string) (*Config, error) { "keyringDir": config.GetKeyringDir(), "p2pDataDir": config.GetP2PDataDir(), "raptorqFilesDir": config.GetRaptorQFilesDir(), - "logLevel": config.LogConfig.Level, }) return &config, nil diff --git a/supernode/node/supernode/server/server.go b/supernode/node/supernode/server/server.go index 157775d8..b9e53060 100644 --- a/supernode/node/supernode/server/server.go +++ b/supernode/node/supernode/server/server.go @@ -47,7 +47,7 @@ func (server *Server) Run(ctx context.Context) error { ctx = logtrace.CtxWithCorrelationID(ctx, server.name) // Set up gRPC logging - logtrace.SetGRPCLogger(ctx) + logtrace.SetGRPCLogger() logtrace.Info(ctx, "Server identity configured", logtrace.Fields{logtrace.FieldModule: "server", "identity": server.config.Identity}) logtrace.Info(ctx, "Server listening", logtrace.Fields{logtrace.FieldModule: "server", "addresses": server.config.ListenAddresses}) diff --git a/tests/system/e2e_cascade_test.go b/tests/system/e2e_cascade_test.go index f52cefea..25f7ca37 100644 --- a/tests/system/e2e_cascade_test.go +++ b/tests/system/e2e_cascade_test.go @@ -3,6 +3,7 @@ package system import ( "bytes" "context" + "crypto/sha256" "encoding/base64" "encoding/json" "fmt" @@ -269,6 +270,10 @@ func TestCascadeE2E(t *testing.T) { require.NoError(t, err, "Failed to read file contents") t.Logf("Read %d bytes from test file", len(data)) + // Calculate SHA256 hash of original file for later comparison + originalHash := sha256.Sum256(data) + t.Logf("Original file SHA256 hash: %x", originalHash) + rqCodec := codec.NewRaptorQCodec(raptorQFilesDir) encodeRes, err := rqCodec.Encode(ctx, codec.EncodeRequest{ @@ -615,9 +620,7 @@ func TestCascadeE2E(t *testing.T) { require.NotEmpty(t, toAddress, "Receiver address should not be empty") require.Equal(t, price, amount, "Payment amount should match action price") - t.Log("Test completed successfully!") - - time.Sleep(120 * time.Second) + time.Sleep(10 * time.Second) outputFileBaseDir := filepath.Join(".") // Try to download the file using the action ID @@ -626,7 +629,7 @@ func TestCascadeE2E(t *testing.T) { t.Logf("Download response: %s", dtaskID) require.NoError(t, err, "Failed to download cascade data using action ID") - time.Sleep(4 * time.Second) + time.Sleep(10 * time.Second) // --------------------------------------- // Step 11: Validate downloaded files exist @@ -661,6 +664,53 @@ func TestCascadeE2E(t *testing.T) { // Validate that at least one file was downloaded require.True(t, fileCount >= 1, "Expected at least 1 file in download directory %s, found %d files", expectedDownloadDir, fileCount) + // --------------------------------------- + // Step 12: Validate downloaded file content matches original + // --------------------------------------- + t.Log("Step 12: Validating downloaded file content matches original file") + + // Find and verify the downloaded file that matches our original test file + var downloadedFilePath string + for _, fileName := range fileNames { + filePath := filepath.Join(expectedDownloadDir, fileName) + // Check if this is our test file by comparing base name + if filepath.Base(fileName) == filepath.Base(testFileFullpath) { + downloadedFilePath = filePath + break + } + } + + if downloadedFilePath == "" { + // If exact name match not found, use the first file (common in single-file downloads) + if len(fileNames) > 0 { + downloadedFilePath = filepath.Join(expectedDownloadDir, fileNames[0]) + t.Logf("Using first downloaded file for verification: %s", fileNames[0]) + } else { + t.Fatalf("No files found in download directory for content verification") + } + } + + // Read the downloaded file + downloadedFile, err := os.Open(downloadedFilePath) + require.NoError(t, err, "Failed to open downloaded file: %s", downloadedFilePath) + defer downloadedFile.Close() + + // Read downloaded file content + downloadedData, err := io.ReadAll(downloadedFile) + require.NoError(t, err, "Failed to read downloaded file content") + + // Calculate SHA256 hash of downloaded file + downloadedHash := sha256.Sum256(downloadedData) + t.Logf("Downloaded file SHA256 hash: %x", downloadedHash) + + // Compare file sizes + require.Equal(t, len(data), len(downloadedData), "Downloaded file size should match original file size") + + // Compare file hashes + require.Equal(t, originalHash, downloadedHash, "Downloaded file hash should match original file hash") + + t.Logf("File verification successful: downloaded file content matches original file") + status, err := actionClient.GetSupernodeStatus(ctx, "lumera1cjyc4ruq739e2lakuhargejjkr0q5vg6x3d7kp") t.Logf("Supernode status: %+v", status) require.NoError(t, err, "Failed to get supernode status") diff --git a/tests/system/go.mod b/tests/system/go.mod index 4f9ee576..38ce381a 100644 --- a/tests/system/go.mod +++ b/tests/system/go.mod @@ -30,6 +30,7 @@ require ( github.com/LumeraProtocol/lumera v1.6.0 github.com/LumeraProtocol/supernode v0.0.0-00010101000000-000000000000 github.com/cometbft/cometbft v0.38.17 + github.com/cosmos/btcutil v1.0.5 github.com/tidwall/gjson v1.14.2 github.com/tidwall/sjson v1.2.5 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 @@ -66,7 +67,6 @@ require ( github.com/cockroachdb/redact v1.1.6 // indirect github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect github.com/cometbft/cometbft-db v0.14.1 // indirect - github.com/cosmos/btcutil v1.0.5 // indirect github.com/cosmos/cosmos-db v1.1.1 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/ics23/go v0.11.0 // indirect @@ -157,6 +157,7 @@ require ( go.opencensus.io v0.24.0 // indirect go.uber.org/mock v0.5.2 // indirect go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect golang.org/x/arch v0.15.0 // indirect golang.org/x/crypto v0.37.0 // indirect golang.org/x/net v0.39.0 // indirect diff --git a/tests/system/go.sum b/tests/system/go.sum index ba702050..2e5a9978 100644 --- a/tests/system/go.sum +++ b/tests/system/go.sum @@ -842,6 +842,8 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -853,6 +855,8 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=