diff --git a/Block/gRPCclient.go b/Block/gRPCclient.go index 6202a99c..109cb631 100644 --- a/Block/gRPCclient.go +++ b/Block/gRPCclient.go @@ -800,15 +800,11 @@ func convertToPbTransaction(tx *config.Transaction, txHash string) *pb.Transacti // Assuming tx.Timestamp is already a Unix timestamp (uint64) pbTx.Timestamp = uint64(tx.Timestamp) } - // Handle transaction fee fields based on type - if tx.Type == 1 || (tx.MaxFee != nil && tx.MaxPriorityFee != nil) { - pbTx.Type = 2 // EIP-1559 - } else { - pbTx.Type = 0 // Legacy - // For legacy transactions, use GasPrice as MaxFee if MaxFee is not set. - if pbTx.MaxFee == "0" && tx.GasPrice != nil { - pbTx.MaxFee = tx.GasPrice.String() - } + // Set type directly from config.Transaction.Type (0=Legacy, 1=AccessList, 2=EIP-1559) + pbTx.Type = uint32(tx.Type) + // For legacy transactions, fall back to GasPrice as MaxFee if MaxFee is unset. + if tx.Type == 0 && pbTx.MaxFee == "0" && tx.GasPrice != nil { + pbTx.MaxFee = tx.GasPrice.String() } return pbTx diff --git a/README.md b/README.md index 13111c20..fda754b3 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ sudo ./Scripts/setup_dependencies.sh For full setup including configuration, firewall rules, and systemd service installation, see **[GETTING_STARTED.md](./GETTING_STARTED.md)**. -> **Docker** (v1.2.0+, experimental): Container-based deployment is available for operators who prefer it. See [DOCKER.md](./DOCKER.md). Source deployment is the recommended path for production nodes. +> **Docker** (v1.2.0+): Container-based deployment is available for operators who prefer it. See [DOCKER.md](./DOCKER.md) for the full setup guide. ## Running a Node diff --git a/Security/Security.go b/Security/Security.go index 905dfedf..f41fe083 100644 --- a/Security/Security.go +++ b/Security/Security.go @@ -3,22 +3,20 @@ package Security import ( "bytes" "context" - "encoding/binary" "errors" "fmt" "math/big" + "sync" + "time" + "gossipnode/DB_OPs" "gossipnode/config" - "time" - "github.com/JupiterMetaLabs/ion" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "go.opentelemetry.io/otel/attribute" - - "gossipnode/DB_OPs" ) const ( @@ -32,28 +30,47 @@ const ( // expectedChainID holds the node's configured chain ID for validation. // Set this at startup using SetExpectedChainID/SetExpectedChainIDBig. -var expectedChainID *big.Int +// signerMu guards expectedChainID and all cached signers. +// These are set once at startup (before serving begins), so contention is negligible +// in practice — the mutex exists purely to satisfy the Go memory model and race detector. +var ( + signerMu sync.RWMutex + expectedChainID *big.Int +) + +// Cached signers — built once when expectedChainID is set; avoids per-tx allocation. +var ( + cachedLatestSigner types.Signer + cachedEIP155Signer types.Signer + cachedHomeSteadSigner types.Signer = types.HomesteadSigner{} +) + +func rebuildSignerCache() { + // caller must hold signerMu.Lock() + if expectedChainID != nil { + cachedLatestSigner = types.LatestSignerForChainID(expectedChainID) + cachedEIP155Signer = types.NewEIP155Signer(expectedChainID) + } +} // SetExpectedChainID sets the expected chain ID used to validate incoming transactions. func SetExpectedChainID(id int) { + signerMu.Lock() + defer signerMu.Unlock() expectedChainID = big.NewInt(int64(id)) + rebuildSignerCache() } // SetExpectedChainIDBig sets the expected chain ID from a big.Int safely. func SetExpectedChainIDBig(id *big.Int) { + signerMu.Lock() + defer signerMu.Unlock() if id == nil { expectedChainID = nil return } - expectedChainID = new(big.Int).Set(id) - - // Convert to uint64 safely - chainIDUint := expectedChainID.Uint64() - - // Convert to binary (big-endian) representation - chainIDBytes := make([]byte, 8) - binary.BigEndian.PutUint64(chainIDBytes, chainIDUint) + rebuildSignerCache() } func CheckZKBlockValidation(zkBlock *config.ZKBlock) (bool, error) { @@ -351,10 +368,14 @@ func allChecksWithConn(tx *config.Transaction, security_cache *SecurityCache, ma } if tx != nil { + toAddr := "nil" // contract deployment + if tx.To != nil { + toAddr = tx.To.Hex() + } span.SetAttributes( attribute.String("tx_hash", tx.Hash.Hex()), attribute.String("from_address", tx.From.Hex()), - attribute.String("to_address", tx.To.Hex()), + attribute.String("to_address", toAddr), attribute.Int64("nonce", int64(tx.Nonce)), ) } @@ -363,7 +384,10 @@ func allChecksWithConn(tx *config.Transaction, security_cache *SecurityCache, ma // 1. ChainID validation _, chainIDSpan := tracer.Start(spanCtx, "Security.allChecksWithCache.validateChainID") // 1.1. ChainID validation: expected chain ID must be configured first - if expectedChainID == nil { + signerMu.RLock() + localExpectedChainID := expectedChainID + signerMu.RUnlock() + if localExpectedChainID == nil { err := errors.New("expected chain ID is not configured") chainIDSpan.RecordError(err) chainIDSpan.SetAttributes(attribute.String("status", "validation_failed")) @@ -389,16 +413,16 @@ func allChecksWithConn(tx *config.Transaction, security_cache *SecurityCache, ma } // 1.3. Transaction ChainID must match expected ChainID - if tx.ChainID.Cmp(expectedChainID) != 0 { + if tx.ChainID.Cmp(localExpectedChainID) != 0 { err := fmt.Errorf("chain ID mismatch: got %s (uint64: %d), expected %s (uint64: %d)", - tx.ChainID.String(), tx.ChainID.Uint64(), expectedChainID.String(), expectedChainID.Uint64()) + tx.ChainID.String(), tx.ChainID.Uint64(), localExpectedChainID.String(), localExpectedChainID.Uint64()) chainIDSpan.RecordError(err) chainIDSpan.SetAttributes( attribute.String("status", "validation_failed"), attribute.String("tx_chain_id", tx.ChainID.String()), attribute.Int64("tx_chain_id_uint64", int64(tx.ChainID.Uint64())), - attribute.String("expected_chain_id", expectedChainID.String()), - attribute.Int64("expected_chain_id_uint64", int64(expectedChainID.Uint64())), + attribute.String("expected_chain_id", localExpectedChainID.String()), + attribute.Int64("expected_chain_id_uint64", int64(localExpectedChainID.Uint64())), ) chainIDSpan.End() span.RecordError(err) @@ -406,8 +430,8 @@ func allChecksWithConn(tx *config.Transaction, security_cache *SecurityCache, ma logger().Error(spanCtx, "Chain ID mismatch", err, ion.String("tx_chain_id", tx.ChainID.String()), ion.Int64("tx_chain_id_uint64", int64(tx.ChainID.Uint64())), - ion.String("expected_chain_id", expectedChainID.String()), - ion.Int64("expected_chain_id_uint64", int64(expectedChainID.Uint64())), + ion.String("expected_chain_id", localExpectedChainID.String()), + ion.Int64("expected_chain_id_uint64", int64(localExpectedChainID.Uint64())), ion.String("function", "Security.allChecksWithCache")) return false, err } @@ -531,6 +555,10 @@ func allChecksWithConn(tx *config.Transaction, security_cache *SecurityCache, ma attribute.Int64("submitted_nonce", int64(tx.Nonce)), ) + // TODO(nonce-gap): currently accepts future nonces (tx.Nonce > expectedNonce). + // If such a tx is committed the account jumps to tx.Nonce+1, permanently orphaning + // all nonces in between. Evaluate whether to enforce tx.Nonce == expectedNonce + // (strict sequential, standard EVM) or keep >= for queued-tx support. if tx.Nonce < expectedNonce { err := fmt.Errorf("submitted nonce %d is too low, expected >= %d", tx.Nonce, expectedNonce) nonceSpan.RecordError(err) @@ -584,8 +612,9 @@ func CheckSignature(tx *config.Transaction, traceCtx context.Context) (bool, err span.SetAttributes(attribute.String("to_address", tx.To.Hex())) } - if tx.From == nil || tx.To == nil || tx.V == nil || tx.R == nil || tx.S == nil { - err := errors.New("transaction missing required signature fields (From, To, V, R, or S)") + // To is nil for contract deployments — only require From + signature fields. + if tx.From == nil || tx.V == nil || tx.R == nil || tx.S == nil { + err := errors.New("transaction missing required signature fields (From, V, R, or S)") span.RecordError(err) span.SetAttributes(attribute.String("status", "validation_failed")) logger().Error(spanCtx, "Transaction missing required signature fields", err, @@ -593,13 +622,11 @@ func CheckSignature(tx *config.Transaction, traceCtx context.Context) (bool, err return false, err } + // Use tx.Type directly — already set by convertEthTxToConfigTx at ingest time. + // Field-presence heuristics are fragile and redundant. var ethTx *types.Transaction - var signer types.Signer - - // Determine transaction type based on fields - switch { - case tx.MaxFee != nil && tx.MaxPriorityFee != nil: - // EIP-1559 (Type 2) + switch tx.Type { + case types.DynamicFeeTxType: // 2 — EIP-1559 inner := &types.DynamicFeeTx{ ChainID: tx.ChainID, Nonce: tx.Nonce, @@ -617,8 +644,7 @@ func CheckSignature(tx *config.Transaction, traceCtx context.Context) (bool, err ethTx = types.NewTx(inner) span.SetAttributes(attribute.String("tx_type", "EIP-1559")) - case len(tx.AccessList) > 0: - // EIP-2930 (Type 1) + case types.AccessListTxType: // 1 — EIP-2930 inner := &types.AccessListTx{ ChainID: tx.ChainID, Nonce: tx.Nonce, @@ -635,8 +661,7 @@ func CheckSignature(tx *config.Transaction, traceCtx context.Context) (bool, err ethTx = types.NewTx(inner) span.SetAttributes(attribute.String("tx_type", "EIP-2930")) - default: - // Legacy (Type 0) + default: // 0 — Legacy inner := &types.LegacyTx{ Nonce: tx.Nonce, To: tx.To, @@ -652,78 +677,78 @@ func CheckSignature(tx *config.Transaction, traceCtx context.Context) (bool, err span.SetAttributes(attribute.String("tx_type", "Legacy")) } - // 👇 Smart signer detection with fallback for MetaMask compatibility v := tx.V.Uint64() var from common.Address var err error + chainIDStr := "legacy/none" if tx.ChainID != nil { - span.SetAttributes( - attribute.Int64("v_value", int64(v)), - attribute.String("chain_id", tx.ChainID.String()), - ) + chainIDStr = tx.ChainID.String() } + span.SetAttributes( + attribute.Int64("v_value", int64(v)), + attribute.String("chain_id", chainIDStr), + ) logger().Info(spanCtx, "Starting signature check", ion.Int64("v_value", int64(v)), - ion.String("chain_id", tx.ChainID.String()), + ion.String("chain_id", chainIDStr), ion.String("function", "Security.CheckSignature")) - // Strategy: MetaMask signs legacy transactions with V=27/28 (pre-EIP-155) - // Even when ChainID is present, we need to try both signers - if v == 27 || v == 28 { - // First try HomesteadSigner (pre-EIP-155) - this is what MetaMask uses - signer = types.HomesteadSigner{} - logger().Info(spanCtx, "Trying HomesteadSigner (pre-EIP-155, MetaMask standard)", + // Signer selection — use cached singletons (built at SetExpectedChainID time). + // Typed txns (type 1/2): V is just the recovery bit (0/1); EIP155Signer rejects them. + // Legacy txns: V encodes chain ID (EIP-155) or is 27/28 (Homestead/pre-EIP-155). + signerMu.RLock() + localLatest := cachedLatestSigner + localEIP155 := cachedEIP155Signer + signerMu.RUnlock() + + switch tx.Type { + case types.DynamicFeeTxType, types.AccessListTxType: + signer := localLatest + if signer == nil { + signer = types.LatestSignerForChainID(tx.ChainID) + } + logger().Info(spanCtx, "Trying LatestSignerForChainID (typed transaction)", + ion.Int64("tx_type", int64(tx.Type)), ion.String("function", "Security.CheckSignature")) from, err = types.Sender(signer, ethTx) if err == nil && from == *tx.From { duration := time.Since(startTime).Seconds() - span.SetAttributes( - attribute.String("status", "success"), - attribute.String("signer_type", "HomesteadSigner"), - attribute.Float64("duration", duration), - ) - logger().Info(spanCtx, "Signature verified with HomesteadSigner", + span.SetAttributes(attribute.String("status", "success"), attribute.String("signer_type", "LatestSignerForChainID"), attribute.Float64("duration", duration)) + logger().Info(spanCtx, "Signature verified with LatestSignerForChainID", ion.Float64("duration", duration), ion.String("function", "Security.CheckSignature")) return true, nil } - // If that failed and ChainID is present, try EIP155Signer as fallback - if tx.ChainID != nil && err != nil { - signer = types.NewEIP155Signer(tx.ChainID) - logger().Info(spanCtx, "HomesteadSigner failed, trying EIP155Signer", - ion.String("error", err.Error()), - ion.String("chain_id", tx.ChainID.String()), + default: // legacy + if v == 27 || v == 28 { + logger().Info(spanCtx, "Trying HomesteadSigner (pre-EIP-155)", ion.String("function", "Security.CheckSignature")) - from, err = types.Sender(signer, ethTx) + from, err = types.Sender(cachedHomeSteadSigner, ethTx) if err == nil && from == *tx.From { duration := time.Since(startTime).Seconds() - span.SetAttributes( - attribute.String("status", "success"), - attribute.String("signer_type", "EIP155Signer"), - attribute.Float64("duration", duration), - ) - logger().Info(spanCtx, "Signature verified with EIP155Signer", + span.SetAttributes(attribute.String("status", "success"), attribute.String("signer_type", "HomesteadSigner"), attribute.Float64("duration", duration)) + logger().Info(spanCtx, "Signature verified with HomesteadSigner", ion.Float64("duration", duration), ion.String("function", "Security.CheckSignature")) return true, nil } + // Fall through to EIP155 below } - } else { - // V != 27/28 means EIP-155 encoded (V = chainID*2 + 35 or chainID*2 + 36) - signer = types.NewEIP155Signer(tx.ChainID) - logger().Info(spanCtx, "Trying EIP155Signer (EIP-155 encoded V value)", + + // EIP-155 encoded V (V = chainID*2 + 35/36), or Homestead fallback + eip155 := localEIP155 + if eip155 == nil { + eip155 = types.NewEIP155Signer(tx.ChainID) + } + logger().Info(spanCtx, "Trying EIP155Signer", ion.String("function", "Security.CheckSignature")) - from, err = types.Sender(signer, ethTx) + from, err = types.Sender(eip155, ethTx) if err == nil && from == *tx.From { duration := time.Since(startTime).Seconds() - span.SetAttributes( - attribute.String("status", "success"), - attribute.String("signer_type", "EIP155Signer"), - attribute.Float64("duration", duration), - ) + span.SetAttributes(attribute.String("status", "success"), attribute.String("signer_type", "EIP155Signer"), attribute.Float64("duration", duration)) logger().Info(spanCtx, "Signature verified with EIP155Signer", ion.Float64("duration", duration), ion.String("function", "Security.CheckSignature")) @@ -798,12 +823,11 @@ func checkTransactionHash(tx *config.Transaction, traceCtx context.Context) (boo span.SetAttributes(attribute.String("tx_hash", tx.Hash.Hex())) - // Construct the transaction the same way as CheckSignature does + // Construct the transaction using tx.Type directly (same as CheckSignature). var ethTx *types.Transaction - switch { - case tx.MaxFee != nil && tx.MaxPriorityFee != nil: - // EIP-1559 (Type 2) + switch tx.Type { + case types.DynamicFeeTxType: // 2 — EIP-1559 inner := &types.DynamicFeeTx{ ChainID: tx.ChainID, Nonce: tx.Nonce, @@ -820,8 +844,7 @@ func checkTransactionHash(tx *config.Transaction, traceCtx context.Context) (boo } ethTx = types.NewTx(inner) - case len(tx.AccessList) > 0: - // EIP-2930 (Type 1) + case types.AccessListTxType: // 1 — EIP-2930 inner := &types.AccessListTx{ ChainID: tx.ChainID, Nonce: tx.Nonce, @@ -837,8 +860,7 @@ func checkTransactionHash(tx *config.Transaction, traceCtx context.Context) (boo } ethTx = types.NewTx(inner) - default: - // Legacy (Type 0) + default: // 0 — Legacy inner := &types.LegacyTx{ Nonce: tx.Nonce, To: tx.To, diff --git a/Security/security_cache.go b/Security/security_cache.go index e1235a43..0588aa5d 100644 --- a/Security/security_cache.go +++ b/Security/security_cache.go @@ -108,35 +108,26 @@ func (s *SecurityCache) GetAccount(address common.Address) *DB_OPs.Account { return s.accounts[address.Hex()] } -// CheckAddressExistWithCache checks if sender and receiver exist in the cache. +// CheckAddressExistWithCache checks if sender (and receiver, if not a contract deployment) exist in the cache. +// tx.To == nil is valid for contract deployments — only the sender is required to exist. func (s *SecurityCache) CheckAddressExistWithCache(tx *config.Transaction, traceCtx context.Context) (bool, error) { - if tx.From == nil || tx.To == nil { - return false, errors.New("from or to address is nil") + if tx.From == nil { + return false, errors.New("from address is nil") } - // Check Sender + // Sender MUST exist sender := s.GetAccount(*tx.From) if sender == nil { - // Sender MUST exist return false, fmt.Errorf("sender account %s not found in cache", tx.From.Hex()) } - // Check Receiver - // Receiver might not exist if it's a new account receiving funds? - // Original CheckAddressExist logic checks if DIDAddress is empty or invalid? - // Let's replicate strict check if that's what CheckAddressExist did. - // Looking at CheckAddressExist (I haven't seen it fully but inferred): - // Usually invalid/non-existent receiver is allowed in some chains (creates account), - // but user prompt says "Sender or receiver DID not found". - // If the original required both to exist, let's stick to that. - - receiver := s.GetAccount(*tx.To) - if receiver == nil { - // For now, assuming receiver must exist or be known. - // If the logic permits new accounts, this test might need adjustment. - // Re-reading original `CheckAddressExist` call: - // "Status... sender or receiver DID not found" -> implies both must be found. - return false, fmt.Errorf("receiver account %s not found in cache", tx.To.Hex()) + // tx.To is nil for contract deployments — skip receiver existence check. + // For regular transfers, receiver must be a known DID account. + if tx.To != nil { + receiver := s.GetAccount(*tx.To) + if receiver == nil { + return false, fmt.Errorf("receiver account %s not found in cache", tx.To.Hex()) + } } return true, nil @@ -161,8 +152,18 @@ func (s *SecurityCache) CheckBalanceWithCache(tx *config.Transaction, traceCtx c } // Calculate Total Cost (Value + Gas) + // Type 2 (EIP-1559) has nil GasPrice — use MaxFee as worst-case gas price. cost := new(big.Int).Set(tx.Value) // Value to transfer - gasCost := new(big.Int).Mul(new(big.Int).SetUint64(tx.GasLimit), tx.GasPrice) + var effectiveGasPrice *big.Int + switch { + case tx.GasPrice != nil: + effectiveGasPrice = tx.GasPrice + case tx.MaxFee != nil: + effectiveGasPrice = tx.MaxFee + default: + effectiveGasPrice = new(big.Int) // 0 — will fail balance check if value > balance + } + gasCost := new(big.Int).Mul(new(big.Int).SetUint64(tx.GasLimit), effectiveGasPrice) totalCost := new(big.Int).Add(cost, gasCost) // Check sufficiency diff --git a/gETH/Facade/Service/Interface.go b/gETH/Facade/Service/Interface.go index 79b6b1af..3840c223 100644 --- a/gETH/Facade/Service/Interface.go +++ b/gETH/Facade/Service/Interface.go @@ -9,6 +9,7 @@ import ( type Service interface { ChainID(ctx context.Context) (*big.Int, error) + GetChainIDValue() *big.Int ClientVersion(ctx context.Context) (string, error) BlockNumber(ctx context.Context) (*big.Int, error) BlockByNumber(ctx context.Context, num *big.Int, fullTx bool) (*Types.Block, error) diff --git a/gETH/Facade/Service/Service.go b/gETH/Facade/Service/Service.go index 93b33ce0..921bdeb2 100644 --- a/gETH/Facade/Service/Service.go +++ b/gETH/Facade/Service/Service.go @@ -17,7 +17,6 @@ import ( Utils "gossipnode/gETH/Facade/Service/utils" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rlp" ) // ServiceImpl implements the Service interface @@ -32,6 +31,10 @@ func NewService(chainID int) Service { } } +func (s *ServiceImpl) GetChainIDValue() *big.Int { + return big.NewInt(int64(s.ChainIDValue)) +} + func (s *ServiceImpl) ChainID(ctx context.Context) (*big.Int, error) { // Create a new context with timeout for this operation opCtx, cancel := context.WithTimeout(ctx, 3*time.Second) @@ -113,7 +116,7 @@ func (s *ServiceImpl) GetTransactionCount(ctx context.Context, addr string, bloc fmt.Printf("Failed to log GetTransactionCount: %v\n", logErr) } - return big.NewInt(int64(account.TxNonce)), nil + return new(big.Int).SetUint64(account.TxNonce), nil } func (s *ServiceImpl) BlockByNumber(ctx context.Context, num *big.Int, fullTx bool) (*Types.Block, error) { @@ -248,9 +251,11 @@ func (s *ServiceImpl) SendRawTx(ctx context.Context, rawHex string) (string, err // If JSON parsing fails, try to parse as RLP-encoded transaction fmt.Println(">>>>>> JSON parsing failed, trying RLP parsing") - // Parse RLP-encoded transaction + // Parse transaction — UnmarshalBinary handles all types: + // legacy (no prefix), EIP-2930 (0x01), EIP-1559 (0x02). + // rlp.DecodeBytes cannot handle typed transactions (0x01/0x02 prefix). var ethTx types.Transaction - err = rlp.DecodeBytes(rawBytes, ðTx) + err = ethTx.UnmarshalBinary(rawBytes) if err != nil { if logErr := Logger.LogData(opCtx, fmt.Sprintf("SendRawTx failed to parse RLP transaction: %v", err), "SendRawTx", -1); logErr != nil { fmt.Printf("Failed to log SendRawTx RLP parse error: %v\n", logErr) @@ -288,7 +293,7 @@ func (s *ServiceImpl) SendRawTx(ctx context.Context, rawHex string) (string, err // convertEthTxToConfigTx converts an Ethereum transaction to our config.Transaction format func convertEthTxToConfigTx(ethTx *types.Transaction) config.Transaction { // Get the sender address - from, _ := types.Sender(types.NewEIP155Signer(ethTx.ChainId()), ethTx) + from, _ := types.Sender(types.LatestSignerForChainID(ethTx.ChainId()), ethTx) // Convert to our transaction format tx := config.Transaction{ @@ -323,21 +328,41 @@ func convertEthTxToConfigTx(ethTx *types.Transaction) config.Transaction { // Debugging fmt.Println("Hash: ", tx.Hash.Hex()) fmt.Println("From: ", tx.From.Hex()) - fmt.Println("To: ", tx.To.Hex()) - fmt.Println("Value: ", tx.Value.String()) + if tx.To != nil { + fmt.Println("To: ", tx.To.Hex()) + } else { + fmt.Println("To: nil (contract creation)") + } + if tx.Value != nil { + fmt.Println("Value: ", tx.Value.String()) + } fmt.Println("Type: ", tx.Type) fmt.Println("Timestamp: ", tx.Timestamp) - fmt.Println("ChainID: ", tx.ChainID.String()) + if tx.ChainID != nil { + fmt.Println("ChainID: ", tx.ChainID.String()) + } fmt.Println("Nonce: ", tx.Nonce) fmt.Println("GasLimit: ", tx.GasLimit) - fmt.Println("GasPrice: ", tx.GasPrice.String()) - fmt.Println("MaxFee: ", tx.MaxFee.String()) - fmt.Println("MaxPriorityFee: ", tx.MaxPriorityFee.String()) + if tx.GasPrice != nil { + fmt.Println("GasPrice: ", tx.GasPrice.String()) + } + if tx.MaxFee != nil { + fmt.Println("MaxFee: ", tx.MaxFee.String()) + } + if tx.MaxPriorityFee != nil { + fmt.Println("MaxPriorityFee: ", tx.MaxPriorityFee.String()) + } fmt.Println("Data: ", tx.Data) fmt.Println("AccessList: ", tx.AccessList) - fmt.Println("V: ", tx.V.String()) - fmt.Println("R: ", tx.R.String()) - fmt.Println("S: ", tx.S.String()) + if tx.V != nil { + fmt.Println("V: ", tx.V.String()) + } + if tx.R != nil { + fmt.Println("R: ", tx.R.String()) + } + if tx.S != nil { + fmt.Println("S: ", tx.S.String()) + } return tx } @@ -487,22 +512,31 @@ func (s *ServiceImpl) ReceiptByHash(ctx context.Context, hash string) (map[strin } receiptMap["type"] = "0x" + fmt.Sprintf("%x", txType) - // Add effectiveGasPrice from transaction - if tx != nil { + // Add effectiveGasPrice from transaction. + // EIP-1559: effectiveGasPrice = min(maxFeePerGas, baseFee + maxPriorityFeePerGas) + // baseFee is the network constant (35 gwei) used across all RPC responses. + { + const baseFee = int64(35_000_000_000) var effectiveGasPrice *big.Int - if tx.GasPrice != nil { - // For legacy (Type 0) and EIP-2930 (Type 1), use GasPrice + if tx != nil && tx.GasPrice != nil { + // Legacy (type 0) and EIP-2930 (type 1) effectiveGasPrice = tx.GasPrice - } else if tx.MaxFee != nil { - // For EIP-1559 (Type 2), use MaxFee as effectiveGasPrice - // In a full implementation, this would be min(MaxFee, baseFee + MaxPriorityFee) - // but for simplicity, we use MaxFee - effectiveGasPrice = tx.MaxFee - } - - if effectiveGasPrice != nil { - receiptMap["effectiveGasPrice"] = "0x" + effectiveGasPrice.Text(16) + } else if tx != nil && tx.MaxFee != nil { + tip := tx.MaxPriorityFee + if tip == nil { + tip = big.NewInt(0) + } + basePlusTip := new(big.Int).Add(big.NewInt(baseFee), tip) + if tx.MaxFee.Cmp(basePlusTip) < 0 { + effectiveGasPrice = new(big.Int).Set(tx.MaxFee) + } else { + effectiveGasPrice = basePlusTip + } + } else { + // Fallback: always emit effectiveGasPrice (EIP-1559 requires it) + effectiveGasPrice = big.NewInt(baseFee) } + receiptMap["effectiveGasPrice"] = "0x" + effectiveGasPrice.Text(16) } // Add from and to addresses from transaction @@ -712,68 +746,35 @@ func (s *ServiceImpl) FeeHistory(ctx context.Context, blockCount uint64, newest oldestNum = big.NewInt(0) } - // Initialize result arrays - baseFeePerGas := make([]string, 0, blockCount+1) - gasUsedRatio := make([]float64, 0, blockCount+1) - var rewards [][]string + // JMDN does not implement variable base fees — return the same 35 gwei constant + // used by eth_getBlockByNumber. No DB reads needed. + const baseFeeConstant = "0x826299e00" // 35_000_000_000 wei - // If percentiles are provided, initialize rewards array - if len(perc) > 0 { - rewards = make([][]string, 0, blockCount+1) + count := newestNum.Uint64() - oldestNum.Uint64() + 1 + baseFeePerGas := make([]string, count) + gasUsedRatio := make([]float64, count) + for i := range baseFeePerGas { + baseFeePerGas[i] = baseFeeConstant + gasUsedRatio[i] = 0.5 // neutral placeholder } - // Fetch blocks from newest to oldest (inclusive) - current := new(big.Int).Set(newestNum) - blocksToFetch := blockCount + 1 - - for i := uint64(0); i < blocksToFetch && current.Sign() >= 0 && current.Cmp(oldestNum) >= 0; i++ { - block, err := s.BlockByNumber(ctx, current, false) - if err != nil { - // If block doesn't exist, skip it - current.Sub(current, big.NewInt(1)) - continue - } - - // Extract base fee per gas - var baseFeeHex string - if len(block.Header.BaseFee) > 0 { - baseFeeBig := new(big.Int).SetBytes(block.Header.BaseFee) - baseFeeHex = "0x" + baseFeeBig.Text(16) - } else { - // If no base fee (pre-EIP-1559), set to 0x0 - baseFeeHex = "0x0" - } - baseFeePerGas = append(baseFeePerGas, baseFeeHex) - - // Calculate gas used ratio - var gasUsedRatioVal float64 - if block.Header.GasLimit > 0 { - gasUsedRatioVal = float64(block.Header.GasUsed) / float64(block.Header.GasLimit) - } else { - gasUsedRatioVal = 0.0 - } - gasUsedRatio = append(gasUsedRatio, gasUsedRatioVal) - - // Calculate rewards if percentiles are provided - if len(perc) > 0 { - blockRewards := make([]string, len(perc)) - // TODO: Calculate actual rewards from transaction priority fees - // For now, set all rewards to 0x0 - for j := range perc { - blockRewards[j] = "0x0" + var rewards [][]string + if len(perc) > 0 { + rewards = make([][]string, count) + for i := range rewards { + row := make([]string, len(perc)) + for j := range row { + row[j] = "0x0" } - rewards = append(rewards, blockRewards) + rewards[i] = row } - - // Move to previous block - current.Sub(current, big.NewInt(1)) } // Build result map result := map[string]any{ - "oldestBlock": fmt.Sprintf("0x%x", oldestNum.Uint64()), - // "baseFeePerGas": baseFeePerGas, - "gasUsedRatio": gasUsedRatio, + "oldestBlock": fmt.Sprintf("0x%x", oldestNum.Uint64()), + "baseFeePerGas": baseFeePerGas, + "gasUsedRatio": gasUsedRatio, } // Add rewards if provided diff --git a/gETH/Facade/Service/Types/Config.go b/gETH/Facade/Service/Types/Config.go index f5cb3c47..2eb7cc77 100644 --- a/gETH/Facade/Service/Types/Config.go +++ b/gETH/Facade/Service/Types/Config.go @@ -60,6 +60,7 @@ type Tx struct { MaxPriorityFeePerGas []byte `json:"maxpriorityfeepergas"` MaxFeePerBlobGas []byte `json:"maxfeeperblobgas"` BlobVersionedHashes [][]byte `json:"blobversionedhashes"` + ChainID []byte `json:"chainId"` BlockNumber *uint64 `json:"blocknumber,omitempty"` BlockHash []byte `json:"blockhash,omitempty"` TransactionIndex *uint64 `json:"transactionindex,omitempty"` @@ -79,10 +80,12 @@ type Log struct { } type CallMsg struct { - From, To string - Data []byte - Value *big.Int - Gas, GasPrice *big.Int + From, To string + Data []byte + Value *big.Int + Gas, GasPrice *big.Int + MaxFeePerGas *big.Int + MaxPriorityFeePerGas *big.Int } type FilterQuery struct { diff --git a/gETH/Facade/Service/utils/utils.go b/gETH/Facade/Service/utils/utils.go index a57cd3d1..54b73410 100644 --- a/gETH/Facade/Service/utils/utils.go +++ b/gETH/Facade/Service/utils/utils.go @@ -111,6 +111,11 @@ func ConvertTrabsactionToTx(tx *config.Transaction) *Types.Tx { v = 0 } + var chainID []byte + if tx.ChainID != nil { + chainID = tx.ChainID.Bytes() + } + Txn := &Types.Tx{ Hash: tx.Hash.Bytes(), From: from, @@ -129,6 +134,7 @@ func ConvertTrabsactionToTx(tx *config.Transaction) *Types.Tx { MaxPriorityFeePerGas: maxPriorityFeePerGas, MaxFeePerBlobGas: nil, BlobVersionedHashes: nil, + ChainID: chainID, } return Txn } diff --git a/gETH/Facade/rpc/handlers.go b/gETH/Facade/rpc/handlers.go index 35891495..349cf63e 100644 --- a/gETH/Facade/rpc/handlers.go +++ b/gETH/Facade/rpc/handlers.go @@ -103,7 +103,7 @@ func (handler *Handlers) Handle(ctx context.Context, req Request) (Response, err log.Printf("📤 RPC Response: %s -> %+v", req.Method, resp) return resp, err } - resp, _ := finish(req, marshalBlock(b, full), nil) + resp, _ := finish(req, marshalBlock(b, full, handler.service.GetChainIDValue()), nil) log.Printf("📤 RPC Response: %s -> %+v", req.Method, resp) return resp, nil @@ -218,7 +218,7 @@ func (handler *Handlers) Handle(ctx context.Context, req Request) (Response, err log.Printf("📤 RPC Response: %s -> %+v", req.Method, resp) return resp, err } - resp, _ := finish(req, marshalTx(tx), nil) + resp, _ := finish(req, marshalTx(tx, handler.service.GetChainIDValue()), nil) log.Printf("📤 RPC Response: %s -> %+v", req.Method, resp) return resp, nil @@ -278,69 +278,69 @@ func (handler *Handlers) Handle(ctx context.Context, req Request) (Response, err // log.Printf("📤 RPC Response: %s -> %+v", req.Method, resp) // return resp, nil - // case "eth_feeHistory": - // if len(req.Params) < 2 { - // resp, _ := invalidParams(req, "missing blockCount and newestBlock") - // log.Printf("📤 RPC Response: %s -> %+v", req.Method, resp) - // return resp, nil - // } + case "eth_feeHistory": + if len(req.Params) < 2 { + resp, _ := invalidParams(req, "missing blockCount and newestBlock") + log.Printf("📤 RPC Response: %s -> %+v", req.Method, resp) + return resp, nil + } - // // Parse blockCount (can be string hex or number) - // var blockCount uint64 - // switch v := req.Params[0].(type) { - // case string: - // if strings.HasPrefix(v, "0x") { - // bigVal := new(big.Int) - // bigVal.SetString(v[2:], 16) - // blockCount = bigVal.Uint64() - // } else { - // fmt.Sscanf(v, "%d", &blockCount) - // } - // case float64: - // blockCount = uint64(v) - // case int: - // blockCount = uint64(v) - // default: - // resp, _ := invalidParams(req, "invalid blockCount type") - // log.Printf("📤 RPC Response: %s -> %+v", req.Method, resp) - // return resp, nil - // } + // Parse blockCount (can be string hex or number) + var blockCount uint64 + switch v := req.Params[0].(type) { + case string: + if strings.HasPrefix(v, "0x") { + bigVal := new(big.Int) + bigVal.SetString(v[2:], 16) + blockCount = bigVal.Uint64() + } else { + fmt.Sscanf(v, "%d", &blockCount) + } + case float64: + blockCount = uint64(v) + case int: + blockCount = uint64(v) + default: + resp, _ := invalidParams(req, "invalid blockCount type") + log.Printf("📤 RPC Response: %s -> %+v", req.Method, resp) + return resp, nil + } - // // Parse newestBlock (block tag) - // newestBlock, err := parseBlockTag(ctx, handler.service, mustString(req.Params[1])) - // if err != nil { - // resp, _ := finish(req, nil, err) - // log.Printf("📤 RPC Response: %s -> %+v", req.Method, resp) - // return resp, err - // } + // Parse newestBlock (block tag) + newestBlock, err := parseBlockTag(ctx, handler.service, mustString(req.Params[1])) + if err != nil { + resp, _ := finish(req, nil, err) + log.Printf("📤 RPC Response: %s -> %+v", req.Method, resp) + return resp, err + } - // // Parse rewardPercentiles (optional, third parameter) - // var rewardPercentiles []float64 - // if len(req.Params) > 2 { - // if percArray, ok := req.Params[2].([]any); ok { - // rewardPercentiles = make([]float64, 0, len(percArray)) - // for _, p := range percArray { - // switch v := p.(type) { - // case float64: - // rewardPercentiles = append(rewardPercentiles, v) - // case string: - // var val float64 - // fmt.Sscanf(v, "%f", &val) - // rewardPercentiles = append(rewardPercentiles, val) - // } - // } - // } - // } + // Parse rewardPercentiles (optional, third parameter) + var rewardPercentiles []float64 + if len(req.Params) > 2 { + if percArray, ok := req.Params[2].([]any); ok { + rewardPercentiles = make([]float64, 0, len(percArray)) + for _, p := range percArray { + switch v := p.(type) { + case float64: + rewardPercentiles = append(rewardPercentiles, v) + case string: + var val float64 + fmt.Sscanf(v, "%f", &val) + rewardPercentiles = append(rewardPercentiles, val) + } + } + } + } - // history, err := handler.service.FeeHistory(ctx, blockCount, newestBlock, rewardPercentiles) - // if err != nil { - // resp, _ := finish(req, nil, err) - // log.Printf("📤 RPC Response: %s -> %+v", req.Method, resp) - // return resp, err - // } - // resp, _ := finish(req, history, nil) - // log.Printf("📤 RPC Response: %s -> %+v", req.Method, resp) - // return resp, nil + history, err := handler.service.FeeHistory(ctx, blockCount, newestBlock, rewardPercentiles) + if err != nil { + resp, _ := finish(req, nil, err) + log.Printf("📤 RPC Response: %s -> %+v", req.Method, resp) + return resp, err + } + resp, _ := finish(req, history, nil) + log.Printf("📤 RPC Response: %s -> %+v", req.Method, resp) + return resp, nil default: resp := RespErr(req.ID, -32601, "Method not found") @@ -421,6 +421,20 @@ func toCallMsg(p any) (Types.CallMsg, error) { msg.GasPrice = bigGasPrice } } + if maxFee, ok := callObj["maxFeePerGas"].(string); ok { + if strings.HasPrefix(maxFee, "0x") { + v := new(big.Int) + v.SetString(maxFee[2:], 16) + msg.MaxFeePerGas = v + } + } + if maxTip, ok := callObj["maxPriorityFeePerGas"].(string); ok { + if strings.HasPrefix(maxTip, "0x") { + v := new(big.Int) + v.SetString(maxTip[2:], 16) + msg.MaxPriorityFeePerGas = v + } + } return msg, nil } @@ -478,7 +492,7 @@ func toFilterQuery(p any) (*Types.FilterQuery, error) { // 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 { +func marshalBlock(b *Types.Block, full bool, globalChainID *big.Int) map[string]any { result := map[string]any{ // Core identity "number": "0x" + new(big.Int).SetUint64(b.Header.Number).Text(16), @@ -523,7 +537,7 @@ func marshalBlock(b *Types.Block, full bool) map[string]any { if full && len(b.Transactions) > 0 { txs := make([]any, len(b.Transactions)) for i, tx := range b.Transactions { - txs[i] = marshalTx(tx) + txs[i] = marshalTx(tx, globalChainID) } result["transactions"] = txs } else if len(b.Transactions) > 0 { @@ -537,19 +551,64 @@ func marshalBlock(b *Types.Block, full bool) map[string]any { return result } -func marshalTx(tx *Types.Tx) map[string]any { +func marshalTx(tx *Types.Tx, globalChainID *big.Int) map[string]any { + // to: null for contract deployments, hex address otherwise + var to any + if len(tx.To) > 0 { + to = "0x" + hex.EncodeToString(tx.To) + } + + // gasPrice: for type 2 use effectiveGasPrice = min(maxFeePerGas, baseFee+tip) + // For legacy/type1 use GasPrice directly. + const baseFee = int64(35_000_000_000) + var gasPriceHex string + if tx.Type == 2 && len(tx.MaxFeePerGas) > 0 { + maxFee := new(big.Int).SetBytes(tx.MaxFeePerGas) + tip := new(big.Int) + if len(tx.MaxPriorityFeePerGas) > 0 { + tip.SetBytes(tx.MaxPriorityFeePerGas) + } + basePlusTip := new(big.Int).Add(big.NewInt(baseFee), tip) + effective := maxFee + if maxFee.Cmp(basePlusTip) > 0 { + effective = basePlusTip + } + gasPriceHex = "0x" + effective.Text(16) + } else if len(tx.GasPrice) > 0 { + gasPriceHex = "0x" + new(big.Int).SetBytes(tx.GasPrice).Text(16) + } else { + gasPriceHex = "0x0" + } + result := map[string]any{ "hash": "0x" + hex.EncodeToString(tx.Hash), "from": "0x" + hex.EncodeToString(tx.From), - "to": "0x" + hex.EncodeToString(tx.To), + "to": to, "input": "0x" + hex.EncodeToString(tx.Input), "value": "0x" + new(big.Int).SetBytes(tx.Value).Text(16), "nonce": "0x" + new(big.Int).SetUint64(tx.Nonce).Text(16), "gas": "0x" + new(big.Int).SetUint64(tx.Gas).Text(16), - "gasPrice": "0x" + new(big.Int).SetBytes(tx.GasPrice).Text(16), + "gasPrice": gasPriceHex, "type": "0x" + new(big.Int).SetUint64(uint64(tx.Type)).Text(16), } + // EIP-1559 fields (type 2) + if tx.Type == 2 { + if len(tx.MaxFeePerGas) > 0 { + result["maxFeePerGas"] = "0x" + new(big.Int).SetBytes(tx.MaxFeePerGas).Text(16) + } + if len(tx.MaxPriorityFeePerGas) > 0 { + result["maxPriorityFeePerGas"] = "0x" + new(big.Int).SetBytes(tx.MaxPriorityFeePerGas).Text(16) + } + } + + // chainId — always emit; fall back to global chain ID if not set on the tx + chainID := new(big.Int).SetBytes(tx.ChainID) + if len(tx.ChainID) == 0 { + chainID = globalChainID + } + result["chainId"] = "0x" + chainID.Text(16) + // Add optional fields if they exist if len(tx.R) > 0 { result["r"] = "0x" + hex.EncodeToString(tx.R) diff --git a/gETH/Facade/rpc/ws_server.go b/gETH/Facade/rpc/ws_server.go index 8d8edf64..47a2856a 100644 --- a/gETH/Facade/rpc/ws_server.go +++ b/gETH/Facade/rpc/ws_server.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log" + "math/big" "net/http" "sync" "time" @@ -138,8 +139,9 @@ func (s *WSServer) handleWS(w http.ResponseWriter, r *http.Request) { } storeSub(subs, &mu, sid, stop) _ = conn.WriteJSON(RespOK(req.ID, sid)) + chainID := s.be.GetChainIDValue() FacadeLocalGRO.Go(GRO.FacadeThread, func(ctx context.Context) error { - forwardBlocks(conn, sid, ch) + forwardBlocks(conn, sid, ch, chainID) return nil }) @@ -219,11 +221,11 @@ type subMsg struct { } `json:"params"` } -func forwardBlocks(conn *websocket.Conn, sid string, ch <-chan *Types.Block) { +func forwardBlocks(conn *websocket.Conn, sid string, ch <-chan *Types.Block, chainID *big.Int) { for b := range ch { msg := subMsg{Jsonrpc: "2.0", Method: "eth_subscription"} msg.Params.Subscription = sid - msg.Params.Result = marshalBlock(b, false) + msg.Params.Result = marshalBlock(b, false, chainID) _ = conn.WriteJSON(msg) } } diff --git a/messaging/BlockProcessing/Processing.go b/messaging/BlockProcessing/Processing.go index 0c118cf8..51d2dcd7 100644 --- a/messaging/BlockProcessing/Processing.go +++ b/messaging/BlockProcessing/Processing.go @@ -791,26 +791,27 @@ func parseTransaction(tx config.Transaction) (*config.ParsedZKTransaction, error // Determine gas fee based on transaction type // Type 0x0 = Legacy, 0x1 = AccessList, 0x2 = DynamicFee (EIP-1559) if tx.Type == 2 { // EIP-1559 transaction - // For EIP-1559, use MaxFee as the effective gas fee - if tx.MaxFee != nil { - parsed.MaxFeeBig = new(big.Int).Set(tx.MaxFee) - parsed.EffectiveGasFee = new(big.Int).Set(tx.MaxFee) + // EIP-1559 effective gas price = min(maxFee, baseFee + tip) + // JMDN uses a flat 35 gwei base fee. + const baseFeeWei = int64(35_000_000_000) + + maxFee := tx.MaxFee + if maxFee == nil { + maxFee = big.NewInt(baseFeeWei) // safe fallback + } + parsed.MaxFeeBig = new(big.Int).Set(maxFee) + + tip := tx.MaxPriorityFee + if tip == nil { + tip = new(big.Int) + } + basePlusTip := new(big.Int).Add(big.NewInt(baseFeeWei), tip) + + // effective = min(maxFee, baseFee + tip) + if maxFee.Cmp(basePlusTip) <= 0 { + parsed.EffectiveGasFee = new(big.Int).Set(maxFee) } else { - // Fallback to MaxPriorityFee if MaxFee is not set - if tx.MaxPriorityFee != nil { - parsed.MaxFeeBig = new(big.Int).Set(tx.MaxPriorityFee) - parsed.EffectiveGasFee = new(big.Int).Set(tx.MaxPriorityFee) - } else { - // Fallback to GasPrice if available - if tx.GasPrice != nil { - parsed.MaxFeeBig = new(big.Int).Set(tx.GasPrice) - parsed.EffectiveGasFee = new(big.Int).Set(tx.GasPrice) - } else { - // Last resort: use default gas price - parsed.MaxFeeBig = big.NewInt(DefaultGasPrice) - parsed.EffectiveGasFee = big.NewInt(DefaultGasPrice) - } - } + parsed.EffectiveGasFee = basePlusTip } } else { // For Legacy or AccessList transactions, use GasPrice if available