Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions Block/gRPCclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
188 changes: 105 additions & 83 deletions Security/Security.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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) {
Expand Down Expand Up @@ -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)),
)
}
Expand All @@ -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"))
Expand All @@ -389,25 +413,25 @@ 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)
span.SetAttributes(attribute.String("status", "validation_failed"))
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
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -584,22 +612,21 @@ 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,
ion.String("function", "Security.CheckSignature"))
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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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"))
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
Loading
Loading