From 56b51c7b2f44946a20e0dcbca8d38945f8358460 Mon Sep 17 00:00:00 2001 From: Christopher Harrison Date: Thu, 5 Feb 2026 19:18:42 +0700 Subject: [PATCH 1/4] core/prioritytransactors: do not panic if the priority transactors contract call goes wrong to preserve liveness --- core/blockchain_reader.go | 6 +-- core/prioritytransactors.go | 88 ++++++++++++++++++++++++------------- core/state_processor.go | 4 +- core/tx_pool.go | 4 +- core/tx_pool_test.go | 2 +- miner/miner_test.go | 4 +- miner/worker.go | 2 +- 7 files changed, 69 insertions(+), 41 deletions(-) diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index 47d85516c..9e522b1a0 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -367,11 +367,11 @@ func (bc *BlockChain) TxLookupLimit() uint64 { return bc.txLookupLimit } -// MustGetPriorityTransactorsForState receives the priority transactor list appropriate for the current state using the contra -func (bc *BlockChain) MustGetPriorityTransactorsForState(header *types.Header, state *state.StateDB) common.PriorityTransactorMap { +// GetPriorityTransactorsForState receives the priority transactor list appropriate for the current state using the contra +func (bc *BlockChain) GetPriorityTransactorsForState(header *types.Header, state *state.StateDB) common.PriorityTransactorMap { blockContext := NewEVMBlockContext(header, bc, nil) vmenv := vm.NewEVM(blockContext, vm.TxContext{}, state, bc.chainConfig, bc.vmConfig) - return MustGetPriorityTransactors(vmenv) + return GetPriorityTransactors(vmenv) } // SubscribeRemovedLogsEvent registers a subscription of RemovedLogsEvent. diff --git a/core/prioritytransactors.go b/core/prioritytransactors.go index 8a8fa142c..09143dfd4 100644 --- a/core/prioritytransactors.go +++ b/core/prioritytransactors.go @@ -1,18 +1,19 @@ package core import ( - "fmt" "strings" "github.com/electroneum/electroneum-sc/accounts/abi" "github.com/electroneum/electroneum-sc/common" "github.com/electroneum/electroneum-sc/contracts/prioritytransactors" "github.com/electroneum/electroneum-sc/core/vm" + "github.com/electroneum/electroneum-sc/log" "github.com/electroneum/electroneum-sc/params" ) // GetPriorityTransactors Gets the priority transactor list for the current state using the priority contract address for the block number passed -func MustGetPriorityTransactors(evm *vm.EVM) common.PriorityTransactorMap { +// GetPriorityTransactors gets the priority transactor list for the current state using the priority contract address for the block number passed. +func GetPriorityTransactors(evm *vm.EVM) common.PriorityTransactorMap { var ( blockNumber = evm.Context.BlockNumber config = evm.ChainConfig() @@ -22,38 +23,65 @@ func MustGetPriorityTransactors(evm *vm.EVM) common.PriorityTransactorMap { result = make(common.PriorityTransactorMap) ) - if address != (common.Address{}) { - // Check if contract code exists at the address. If it doesn't. We haven't deployed the contract yet, so no error needed. - byteCode := evm.StateDB.GetCode(address) - if len(byteCode) == 0 { - return result - } + // No contract configured => no priority transactors + if address == (common.Address{}) { + return result + } - contractABI, _ := abi.JSON(strings.NewReader(prioritytransactors.ETNPriorityTransactorsInterfaceMetaData.ABI)) - input, _ := contractABI.Pack(method) - output, _, err := evm.StaticCall(contract, address, input, params.MaxGasLimit) - // if there is an issue pulling the contract panic as something must be very - // wrong, and we don't want an accidental fork or potentially try again and have - // an incorrect flow - if err != nil { - panic(fmt.Errorf("error getting the priority transactors from the EVM/contract: %s", err)) - } + // Contract not deployed yet => no priority transactors (not an error) + byteCode := evm.StateDB.GetCode(address) + if len(byteCode) == 0 { + return result + } - unpackResult, err := contractABI.Unpack(method, output) - // if there is an issue pulling the contract panic as something must be very - // wrong, and we don't want an accidental fork or potentially try again and have - // an incorrect flow - if err != nil { - panic(fmt.Errorf("error getting the priority transactors from the EVM/contract: %s", err)) - } + contractABI, err := abi.JSON(strings.NewReader(prioritytransactors.ETNPriorityTransactorsInterfaceMetaData.ABI)) + if err != nil { + // ABI parse failure is a software/config issue; safest is disable feature rather than crash the chain + log.Error("PriorityTransactors: failed to parse ABI; disabling priority list for this block", + "err", err, "address", address, "block", blockNumber) + return result + } + + input, err := contractABI.Pack(method) + if err != nil { + log.Error("PriorityTransactors: failed to pack call data; disabling priority list for this block", + "err", err, "address", address, "block", blockNumber) + return result + } - transactorsMeta := abi.ConvertType(unpackResult[0], new([]prioritytransactors.ETNPriorityTransactorsInterfaceTransactorMeta)).(*[]prioritytransactors.ETNPriorityTransactorsInterfaceTransactorMeta) - for _, t := range *transactorsMeta { - result[common.HexToPublicKey(t.PublicKey)] = common.PriorityTransactor{ - IsGasPriceWaiver: t.IsGasPriceWaiver, - EntityName: t.Name, - } + output, _, err := evm.StaticCall(contract, address, input, params.MaxGasLimit) + if err != nil { + // IMPORTANT: on failure, return empty list (deny privileges) instead of panicking (halting chain) + log.Error("PriorityTransactors: contract call failed; disabling priority list for this block", + "err", err, "address", address, "block", blockNumber) + return result + } + + unpackResult, err := contractABI.Unpack(method, output) + if err != nil { + log.Error("PriorityTransactors: ABI unpack failed; disabling priority list for this block", + "err", err, "address", address, "block", blockNumber) + return result + } + + // Defensive: avoid panic if return shape unexpected + if len(unpackResult) == 0 { + log.Error("PriorityTransactors: empty return data; disabling priority list for this block", + "address", address, "block", blockNumber) + return result + } + + transactorsMeta := abi.ConvertType( + unpackResult[0], + new([]prioritytransactors.ETNPriorityTransactorsInterfaceTransactorMeta), + ).(*[]prioritytransactors.ETNPriorityTransactorsInterfaceTransactorMeta) + + for _, t := range *transactorsMeta { + result[common.HexToPublicKey(t.PublicKey)] = common.PriorityTransactor{ + IsGasPriceWaiver: t.IsGasPriceWaiver, + EntityName: t.Name, } } + return result } diff --git a/core/state_processor.go b/core/state_processor.go index 1e6c0f906..704f19bb6 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -72,7 +72,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg } blockContext := NewEVMBlockContext(header, p.bc, nil) vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg) - statedb.SetPriorityTransactors(MustGetPriorityTransactors(vmenv)) + statedb.SetPriorityTransactors(GetPriorityTransactors(vmenv)) // Iterate over and process the individual transactions for i, tx := range block.Transactions() { msg, err := tx.AsMessage(types.MakeSigner(p.config, header.Number), header.BaseFee) @@ -151,7 +151,7 @@ func applyTransaction(msg types.Message, config *params.ChainConfig, bc ChainCon // Update the priority transactor map for the next tx application in the block validation loop if this tx // successfully updated the priority transactor list in the EVM/stateDB. if msg.To() != nil && *msg.To() == config.GetPriorityTransactorsContractAddress(blockNumber) { - statedb.SetPriorityTransactors(MustGetPriorityTransactors(evm)) + statedb.SetPriorityTransactors(GetPriorityTransactors(evm)) } return receipt, err } diff --git a/core/tx_pool.go b/core/tx_pool.go index 479d40e78..41783481b 100644 --- a/core/tx_pool.go +++ b/core/tx_pool.go @@ -160,7 +160,7 @@ type blockChain interface { GetBlock(hash common.Hash, number uint64) *types.Block StateAt(root common.Hash) (*state.StateDB, error) SubscribeChainHeadEvent(ch chan<- ChainHeadEvent) event.Subscription - MustGetPriorityTransactorsForState(header *types.Header, state *state.StateDB) common.PriorityTransactorMap + GetPriorityTransactorsForState(header *types.Header, state *state.StateDB) common.PriorityTransactorMap } // TxPoolConfig are the configuration parameters of the transaction pool. @@ -1421,7 +1421,7 @@ func (pool *TxPool) reset(oldHead, newHead *types.Header) { pool.pendingNonces = newTxNoncer(statedb) pool.currentMaxGas = newHead.GasLimit - pool.currentPriorityTransactors = pool.chain.MustGetPriorityTransactorsForState(newHead, pool.currentState) + pool.currentPriorityTransactors = pool.chain.GetPriorityTransactorsForState(newHead, pool.currentState) // Inject any transactions discarded due to reorgs log.Debug("Reinjecting stale transactions", "count", len(reinject)) diff --git a/core/tx_pool_test.go b/core/tx_pool_test.go index 3c7899c6a..5a5e32251 100644 --- a/core/tx_pool_test.go +++ b/core/tx_pool_test.go @@ -134,7 +134,7 @@ func (bc *testBlockChain) SubscribeChainHeadEvent(ch chan<- ChainHeadEvent) even return bc.chainHeadFeed.Subscribe(ch) } -func (bc *testBlockChain) MustGetPriorityTransactorsForState(header *types.Header, state *state.StateDB) common.PriorityTransactorMap { +func (bc *testBlockChain) GetPriorityTransactorsForState(header *types.Header, state *state.StateDB) common.PriorityTransactorMap { priorityPubkeys := []string{ "04efb99d9860f4dec4cb548a5722c27e9ef58e37fbab9719c5b33d55c216db49311221a01f638ce5f255875b194e0acaa58b19a89d2e56a864427298f826a7f887", "047409b5751867a7a4ac4b3b4358c3f87d97a339b2ab0943217e5dec9aebc10938de4bb7447c26f2eaf4e39417976480b30d2b5c60baccaeb08971840f3bbc282f", diff --git a/miner/miner_test.go b/miner/miner_test.go index e0e5a79aa..25dfde08c 100644 --- a/miner/miner_test.go +++ b/miner/miner_test.go @@ -87,8 +87,8 @@ func (bc *testBlockChain) GetPriorityTransactorsCache() common.PriorityTransacto return common.PriorityTransactorMap{} } -// MustGetPriorityTransactorsForState receives the priority transactor list appropriate for the current state -func (bc *testBlockChain) MustGetPriorityTransactorsForState(header *types.Header, state *state.StateDB) common.PriorityTransactorMap { +// GetPriorityTransactorsForState receives the priority transactor list appropriate for the current state +func (bc *testBlockChain) GetPriorityTransactorsForState(header *types.Header, state *state.StateDB) common.PriorityTransactorMap { return common.PriorityTransactorMap{} } diff --git a/miner/worker.go b/miner/worker.go index f9b08cd47..71646fb26 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -875,7 +875,7 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP } var coalescedLogs []*types.Log - transactors := w.chain.MustGetPriorityTransactorsForState(env.header, env.state) + transactors := w.chain.GetPriorityTransactorsForState(env.header, env.state) for { // In the following three cases, we will interrupt the execution of the transaction. // (1) new head block event arrival, the interrupt signal is 1 From 34e6dc5f105f8ffce960f8d09455fa17f8e1d1cd Mon Sep 17 00:00:00 2001 From: Christopher Harrison Date: Thu, 5 Feb 2026 19:38:51 +0700 Subject: [PATCH 2/4] core: defensively handle malformed priority transactor contract data --- core/prioritytransactors.go | 61 ++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/core/prioritytransactors.go b/core/prioritytransactors.go index 09143dfd4..ae43389c3 100644 --- a/core/prioritytransactors.go +++ b/core/prioritytransactors.go @@ -12,7 +12,6 @@ import ( ) // GetPriorityTransactors Gets the priority transactor list for the current state using the priority contract address for the block number passed -// GetPriorityTransactors gets the priority transactor list for the current state using the priority contract address for the block number passed. func GetPriorityTransactors(evm *vm.EVM) common.PriorityTransactorMap { var ( blockNumber = evm.Context.BlockNumber @@ -71,13 +70,43 @@ func GetPriorityTransactors(evm *vm.EVM) common.PriorityTransactorMap { return result } - transactorsMeta := abi.ConvertType( - unpackResult[0], - new([]prioritytransactors.ETNPriorityTransactorsInterfaceTransactorMeta), - ).(*[]prioritytransactors.ETNPriorityTransactorsInterfaceTransactorMeta) + metas, ok := safeConvertTransactorsMeta(unpackResult[0]) + if !ok { + log.Error("PriorityTransactors: unexpected return type; disabling priority list for this block", + "address", address, "block", blockNumber) + return result + } + + for _, t := range metas { + // Validate public key bytes are exactly 65 bytes (your PublicKey type length). + pkBytes := common.FromHex(t.PublicKey) + if len(pkBytes) != common.PublicKeyLength { + log.Warn("PriorityTransactors: invalid public key length in contract data; skipping entry", + "len", len(pkBytes), + "name", t.Name, + "address", address, + "block", blockNumber) + continue + } + + // Optional: reject all-zero pubkeys (prevents silly/garbage entries) + allZero := true + for _, b := range pkBytes { + if b != 0 { + allZero = false + break + } + } + if allZero { + log.Warn("PriorityTransactors: all-zero public key in contract data; skipping entry", + "name", t.Name, + "address", address, + "block", blockNumber) + continue + } - for _, t := range *transactorsMeta { - result[common.HexToPublicKey(t.PublicKey)] = common.PriorityTransactor{ + pk := common.BytesToPublicKey(pkBytes) + result[pk] = common.PriorityTransactor{ IsGasPriceWaiver: t.IsGasPriceWaiver, EntityName: t.Name, } @@ -85,3 +114,21 @@ func GetPriorityTransactors(evm *vm.EVM) common.PriorityTransactorMap { return result } + +func safeConvertTransactorsMeta(unpack0 any) (metas []prioritytransactors.ETNPriorityTransactorsInterfaceTransactorMeta, ok bool) { + defer func() { + if r := recover(); r != nil { + ok = false + } + }() + + ptr := abi.ConvertType( + unpack0, + new([]prioritytransactors.ETNPriorityTransactorsInterfaceTransactorMeta), + ).(*[]prioritytransactors.ETNPriorityTransactorsInterfaceTransactorMeta) + + if ptr == nil { + return nil, false + } + return *ptr, true +} From 6da811a2169455f72f4b178ec74faf8a683440bf Mon Sep 17 00:00:00 2001 From: Christopher Harrison Date: Fri, 13 Feb 2026 18:01:55 +0700 Subject: [PATCH 3/4] core: add unit tests for safeConvertTransactorsMeta Cover valid input, empty slice, nil, and type-mismatch recovery to ensure the panic-recovery wrapper introduced in the liveness PR behaves correctly for all edge cases. --- core/prioritytransactors_test.go | 106 +++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 core/prioritytransactors_test.go diff --git a/core/prioritytransactors_test.go b/core/prioritytransactors_test.go new file mode 100644 index 000000000..2fd0af9b4 --- /dev/null +++ b/core/prioritytransactors_test.go @@ -0,0 +1,106 @@ +package core + +import ( + "testing" + + "github.com/electroneum/electroneum-sc/contracts/prioritytransactors" +) + +// Test that safeConvertTransactorsMeta returns ok=true and the correct slice +// when given a valid []ETNPriorityTransactorsInterfaceTransactorMeta value. +func TestSafeConvertTransactorsMeta_ValidInput(t *testing.T) { + input := []prioritytransactors.ETNPriorityTransactorsInterfaceTransactorMeta{ + { + IsGasPriceWaiver: true, + PublicKey: "0x04abcd", + Name: "TestEntity", + }, + { + IsGasPriceWaiver: false, + PublicKey: "0x04ef01", + Name: "AnotherEntity", + }, + } + + metas, ok := safeConvertTransactorsMeta(input) + if !ok { + t.Fatal("expected ok=true for valid input") + } + if len(metas) != 2 { + t.Fatalf("expected 2 metas, got %d", len(metas)) + } + if metas[0].Name != "TestEntity" { + t.Errorf("expected Name 'TestEntity', got %q", metas[0].Name) + } + if !metas[0].IsGasPriceWaiver { + t.Error("expected IsGasPriceWaiver=true for first entry") + } + if metas[1].Name != "AnotherEntity" { + t.Errorf("expected Name 'AnotherEntity', got %q", metas[1].Name) + } + if metas[1].IsGasPriceWaiver { + t.Error("expected IsGasPriceWaiver=false for second entry") + } +} + +// Test that safeConvertTransactorsMeta returns ok=true and an empty slice +// when given a valid but empty slice. +func TestSafeConvertTransactorsMeta_EmptySlice(t *testing.T) { + input := []prioritytransactors.ETNPriorityTransactorsInterfaceTransactorMeta{} + + metas, ok := safeConvertTransactorsMeta(input) + if !ok { + t.Fatal("expected ok=true for empty slice input") + } + if len(metas) != 0 { + t.Fatalf("expected 0 metas, got %d", len(metas)) + } +} + +// Test that safeConvertTransactorsMeta returns ok=false and recovers without +// panicking when given nil input. +func TestSafeConvertTransactorsMeta_NilInput(t *testing.T) { + metas, ok := safeConvertTransactorsMeta(nil) + if ok { + t.Fatal("expected ok=false for nil input") + } + if metas != nil { + t.Fatalf("expected nil metas for nil input, got %v", metas) + } +} + +// Test that safeConvertTransactorsMeta returns ok=false and recovers without +// panicking when given an incompatible type (string instead of the expected slice). +func TestSafeConvertTransactorsMeta_WrongType(t *testing.T) { + metas, ok := safeConvertTransactorsMeta("not a slice") + if ok { + t.Fatal("expected ok=false for wrong type input") + } + if metas != nil { + t.Fatalf("expected nil metas for wrong type, got %v", metas) + } +} + +// Test that safeConvertTransactorsMeta returns ok=false and recovers without +// panicking when given an integer (another incompatible type). +func TestSafeConvertTransactorsMeta_IntType(t *testing.T) { + metas, ok := safeConvertTransactorsMeta(42) + if ok { + t.Fatal("expected ok=false for int input") + } + if metas != nil { + t.Fatalf("expected nil metas for int input, got %v", metas) + } +} + +// Test that safeConvertTransactorsMeta returns ok=false and recovers without +// panicking when given an empty struct (incompatible type). +func TestSafeConvertTransactorsMeta_EmptyStruct(t *testing.T) { + metas, ok := safeConvertTransactorsMeta(struct{}{}) + if ok { + t.Fatal("expected ok=false for empty struct input") + } + if metas != nil { + t.Fatalf("expected nil metas for struct input, got %v", metas) + } +} From a08563fcbd5c14b90cb6c8b58cb0da8e98b6bb60 Mon Sep 17 00:00:00 2001 From: Christopher Harrison Date: Fri, 13 Feb 2026 18:05:57 +0700 Subject: [PATCH 4/4] core: validate priority transactor public keys against secp256k1 curve Use PublicKey.IsValid() to reject keys that are 65 bytes but have a wrong prefix or coordinates not on the secp256k1 curve. Add tests for the curve validation covering valid keys, wrong prefix, and off-curve points. --- core/prioritytransactors.go | 7 ++++++ core/prioritytransactors_test.go | 38 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/core/prioritytransactors.go b/core/prioritytransactors.go index ae43389c3..6dd4e88a4 100644 --- a/core/prioritytransactors.go +++ b/core/prioritytransactors.go @@ -106,6 +106,13 @@ func GetPriorityTransactors(evm *vm.EVM) common.PriorityTransactorMap { } pk := common.BytesToPublicKey(pkBytes) + if !pk.IsValid() { + log.Warn("PriorityTransactors: public key not on secp256k1 curve; skipping entry", + "name", t.Name, + "address", address, + "block", blockNumber) + continue + } result[pk] = common.PriorityTransactor{ IsGasPriceWaiver: t.IsGasPriceWaiver, EntityName: t.Name, diff --git a/core/prioritytransactors_test.go b/core/prioritytransactors_test.go index 2fd0af9b4..2e4888ee3 100644 --- a/core/prioritytransactors_test.go +++ b/core/prioritytransactors_test.go @@ -3,6 +3,7 @@ package core import ( "testing" + "github.com/electroneum/electroneum-sc/common" "github.com/electroneum/electroneum-sc/contracts/prioritytransactors" ) @@ -104,3 +105,40 @@ func TestSafeConvertTransactorsMeta_EmptyStruct(t *testing.T) { t.Fatalf("expected nil metas for struct input, got %v", metas) } } + +// Test that PublicKey.IsValid rejects a 65-byte key that starts with 0x04 +// but whose (x, y) coordinates do not lie on the secp256k1 curve. +func TestPublicKeyIsValid_InvalidCurvePoint(t *testing.T) { + // 65 bytes starting with 0x04, but x=1, y=1 is not on secp256k1 + var pk common.PublicKey + pk[0] = 0x04 + pk[1] = 0x01 // x = 1 + pk[33] = 0x01 // y = 1 + + if pk.IsValid() { + t.Error("expected IsValid=false for point not on secp256k1 curve") + } +} + +// Test that PublicKey.IsValid rejects a 65-byte key that does not start +// with the uncompressed prefix 0x04. +func TestPublicKeyIsValid_WrongPrefix(t *testing.T) { + // Take a known-good key and corrupt the prefix byte + validHex := "04efb99d9860f4dec4cb548a5722c27e9ef58e37fbab9719c5b33d55c216db49311221a01f638ce5f255875b194e0acaa58b19a89d2e56a864427298f826a7f887" + pk := common.HexToPublicKey(validHex) + pk[0] = 0x02 // compressed prefix — invalid for uncompressed key + + if pk.IsValid() { + t.Error("expected IsValid=false for key with wrong prefix byte") + } +} + +// Test that PublicKey.IsValid accepts a known-good uncompressed secp256k1 key. +func TestPublicKeyIsValid_ValidKey(t *testing.T) { + validHex := "04efb99d9860f4dec4cb548a5722c27e9ef58e37fbab9719c5b33d55c216db49311221a01f638ce5f255875b194e0acaa58b19a89d2e56a864427298f826a7f887" + pk := common.HexToPublicKey(validHex) + + if !pk.IsValid() { + t.Error("expected IsValid=true for known-good secp256k1 public key") + } +}