diff --git a/cmd/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go
new file mode 100644
index 0000000000000000000000000000000000000000..aecbad79dfb2d01cfea33878283477f078e9cf61
--- /dev/null
+++ b/cmd/evm/internal/t8ntool/transaction.go
@@ -0,0 +1,136 @@
+// Copyright 2021 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package t8ntool
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"math/big"
+	"os"
+	"strings"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/common/hexutil"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/log"
+	"github.com/ethereum/go-ethereum/params"
+	"github.com/ethereum/go-ethereum/rlp"
+	"github.com/ethereum/go-ethereum/tests"
+	"gopkg.in/urfave/cli.v1"
+)
+
+type result struct {
+	Error   error
+	Address common.Address
+	Hash    common.Hash
+}
+
+// MarshalJSON marshals as JSON with a hash.
+func (r *result) MarshalJSON() ([]byte, error) {
+	type xx struct {
+		Error   string          `json:"error,omitempty"`
+		Address *common.Address `json:"address,omitempty"`
+		Hash    *common.Hash    `json:"hash,omitempty"`
+	}
+	var out xx
+	if r.Error != nil {
+		out.Error = r.Error.Error()
+	}
+	if r.Address != (common.Address{}) {
+		out.Address = &r.Address
+	}
+	if r.Hash != (common.Hash{}) {
+		out.Hash = &r.Hash
+	}
+	return json.Marshal(out)
+}
+
+func Transaction(ctx *cli.Context) error {
+	// Configure the go-ethereum logger
+	glogger := log.NewGlogHandler(log.StreamHandler(os.Stderr, log.TerminalFormat(false)))
+	glogger.Verbosity(log.Lvl(ctx.Int(VerbosityFlag.Name)))
+	log.Root().SetHandler(glogger)
+
+	var (
+		err error
+	)
+	// We need to load the transactions. May be either in stdin input or in files.
+	// Check if anything needs to be read from stdin
+	var (
+		txStr       = ctx.String(InputTxsFlag.Name)
+		inputData   = &input{}
+		chainConfig *params.ChainConfig
+	)
+	// Construct the chainconfig
+	if cConf, _, err := tests.GetChainConfig(ctx.String(ForknameFlag.Name)); err != nil {
+		return NewError(ErrorVMConfig, fmt.Errorf("failed constructing chain configuration: %v", err))
+	} else {
+		chainConfig = cConf
+	}
+	// Set the chain id
+	chainConfig.ChainID = big.NewInt(ctx.Int64(ChainIDFlag.Name))
+	var body hexutil.Bytes
+	if txStr == stdinSelector {
+		decoder := json.NewDecoder(os.Stdin)
+		if err := decoder.Decode(inputData); err != nil {
+			return NewError(ErrorJson, fmt.Errorf("failed unmarshaling stdin: %v", err))
+		}
+		// Decode the body of already signed transactions
+		body = common.FromHex(inputData.TxRlp)
+	} else {
+		// Read input from file
+		inFile, err := os.Open(txStr)
+		if err != nil {
+			return NewError(ErrorIO, fmt.Errorf("failed reading txs file: %v", err))
+		}
+		defer inFile.Close()
+		decoder := json.NewDecoder(inFile)
+		if strings.HasSuffix(txStr, ".rlp") {
+			if err := decoder.Decode(&body); err != nil {
+				return err
+			}
+		} else {
+			return NewError(ErrorIO, errors.New("only rlp supported"))
+		}
+	}
+	signer := types.MakeSigner(chainConfig, new(big.Int))
+	// We now have the transactions in 'body', which is supposed to be an
+	// rlp list of transactions
+	it, err := rlp.NewListIterator([]byte(body))
+	if err != nil {
+		return err
+	}
+	var results []result
+	for it.Next() {
+		var tx types.Transaction
+		err := rlp.DecodeBytes(it.Value(), &tx)
+		if err != nil {
+			results = append(results, result{Error: err})
+			continue
+		}
+		sender, err := types.Sender(signer, &tx)
+		if err != nil {
+			results = append(results, result{Error: err})
+			continue
+		}
+		results = append(results, result{Address: sender, Hash: tx.Hash()})
+	}
+	out, err := json.MarshalIndent(results, "", "  ")
+	fmt.Println(string(out))
+	return err
+}
diff --git a/cmd/evm/internal/t8ntool/transition.go b/cmd/evm/internal/t8ntool/transition.go
index 54596226905109ca73bc4ebde525c9657cb3d6a4..84da89bee169da8e7cc71aedf632ecb481e2900f 100644
--- a/cmd/evm/internal/t8ntool/transition.go
+++ b/cmd/evm/internal/t8ntool/transition.go
@@ -81,7 +81,7 @@ type input struct {
 	TxRlp string            `json:"txsRlp,omitempty"`
 }
 
-func Main(ctx *cli.Context) error {
+func Transition(ctx *cli.Context) error {
 	// Configure the go-ethereum logger
 	glogger := log.NewGlogHandler(log.StreamHandler(os.Stderr, log.TerminalFormat(false)))
 	glogger.Verbosity(log.Lvl(ctx.Int(VerbosityFlag.Name)))
diff --git a/cmd/evm/main.go b/cmd/evm/main.go
index c32b6c38219a98ca0cfa32c9177932bbd2ed70f0..f1bbdb5d761f10b73d7f058201aaada69cb89b95 100644
--- a/cmd/evm/main.go
+++ b/cmd/evm/main.go
@@ -135,7 +135,7 @@ var stateTransitionCommand = cli.Command{
 	Name:    "transition",
 	Aliases: []string{"t8n"},
 	Usage:   "executes a full state transition",
-	Action:  t8ntool.Main,
+	Action:  t8ntool.Transition,
 	Flags: []cli.Flag{
 		t8ntool.TraceFlag,
 		t8ntool.TraceDisableMemoryFlag,
@@ -154,6 +154,18 @@ var stateTransitionCommand = cli.Command{
 		t8ntool.VerbosityFlag,
 	},
 }
+var transactionCommand = cli.Command{
+	Name:    "transaction",
+	Aliases: []string{"t9n"},
+	Usage:   "performs transaction validation",
+	Action:  t8ntool.Transaction,
+	Flags: []cli.Flag{
+		t8ntool.InputTxsFlag,
+		t8ntool.ChainIDFlag,
+		t8ntool.ForknameFlag,
+		t8ntool.VerbosityFlag,
+	},
+}
 
 func init() {
 	app.Flags = []cli.Flag{
@@ -187,6 +199,7 @@ func init() {
 		runCommand,
 		stateTestCommand,
 		stateTransitionCommand,
+		transactionCommand,
 	}
 	cli.CommandHelpTemplate = flags.OriginCommandHelpTemplate
 }
diff --git a/cmd/evm/t8n_test.go b/cmd/evm/t8n_test.go
index 5af7ee19cc0b879bdff8451f1c7a1c043417710d..ad518a91a22a0ad07fef70017335833bfebb2b8f 100644
--- a/cmd/evm/t8n_test.go
+++ b/cmd/evm/t8n_test.go
@@ -70,7 +70,6 @@ type t8nOutput struct {
 }
 
 func (args *t8nOutput) get() (out []string) {
-	out = append(out, "t8n")
 	if args.body {
 		out = append(out, "--output.body", "stdout")
 	} else {
@@ -173,7 +172,9 @@ func TestT8n(t *testing.T) {
 		},
 	} {
 
-		args := append(tc.output.get(), tc.input.get(tc.base)...)
+		args := []string{"t8n"}
+		args = append(args, tc.output.get()...)
+		args = append(args, tc.input.get(tc.base)...)
 		tt.Run("evm-test", args...)
 		tt.Logf("args: %v\n", strings.Join(args, " "))
 		// Compare the expected output, if provided
@@ -198,6 +199,86 @@ func TestT8n(t *testing.T) {
 	}
 }
 
+type t9nInput struct {
+	inTxs  string
+	stFork string
+}
+
+func (args *t9nInput) get(base string) []string {
+	var out []string
+	if opt := args.inTxs; opt != "" {
+		out = append(out, "--input.txs")
+		out = append(out, fmt.Sprintf("%v/%v", base, opt))
+	}
+	if opt := args.stFork; opt != "" {
+		out = append(out, "--state.fork", opt)
+	}
+	return out
+}
+
+func TestT9n(t *testing.T) {
+	tt := new(testT8n)
+	tt.TestCmd = cmdtest.NewTestCmd(t, tt)
+	for i, tc := range []struct {
+		base        string
+		input       t9nInput
+		expExitCode int
+		expOut      string
+	}{
+		{ // London txs on homestead
+			base: "./testdata/15",
+			input: t9nInput{
+				inTxs:  "signed_txs.rlp",
+				stFork: "Homestead",
+			},
+			expOut: "exp.json",
+		},
+		{ // London txs on homestead
+			base: "./testdata/15",
+			input: t9nInput{
+				inTxs:  "signed_txs.rlp",
+				stFork: "London",
+			},
+			expOut: "exp2.json",
+		},
+		{ // An RLP list (a blockheader really)
+			base: "./testdata/15",
+			input: t9nInput{
+				inTxs:  "blockheader.rlp",
+				stFork: "London",
+			},
+			expOut: "exp3.json",
+		},
+	} {
+
+		args := []string{"t9n"}
+		args = append(args, tc.input.get(tc.base)...)
+
+		tt.Run("evm-test", args...)
+		tt.Logf("args:\n go run . %v\n", strings.Join(args, " "))
+		// Compare the expected output, if provided
+		if tc.expOut != "" {
+			want, err := os.ReadFile(fmt.Sprintf("%v/%v", tc.base, tc.expOut))
+			if err != nil {
+				t.Fatalf("test %d: could not read expected output: %v", i, err)
+			}
+			have := tt.Output()
+			ok, err := cmpJson(have, want)
+			switch {
+			case err != nil:
+				t.Logf(string(have))
+				t.Fatalf("test %d, json parsing failed: %v", i, err)
+			case !ok:
+				t.Fatalf("test %d: output wrong, have \n%v\nwant\n%v\n", i, string(have), string(want))
+			}
+		}
+		tt.WaitExit()
+		if have, want := tt.ExitStatus(), tc.expExitCode; have != want {
+			t.Fatalf("test %d: wrong exit code, have %d, want %d", i, have, want)
+		}
+	}
+}
+
 // cmpJson compares the JSON in two byte slices.
 func cmpJson(a, b []byte) (bool, error) {
 	var j, j2 interface{}
diff --git a/cmd/evm/testdata/15/blockheader.rlp b/cmd/evm/testdata/15/blockheader.rlp
new file mode 100644
index 0000000000000000000000000000000000000000..1124e8e2da92907b22613243001c1719fe8cda72
--- /dev/null
+++ b/cmd/evm/testdata/15/blockheader.rlp
@@ -0,0 +1 @@
+"0xf901f0a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007b0101020383010203a00000000000000000000000000000000000000000000000000000000000000000880000000000000000"
\ No newline at end of file
diff --git a/cmd/evm/testdata/15/exp.json b/cmd/evm/testdata/15/exp.json
new file mode 100644
index 0000000000000000000000000000000000000000..03d970c5655cad59a1fd98b7610d8e04a37c9f26
--- /dev/null
+++ b/cmd/evm/testdata/15/exp.json
@@ -0,0 +1,8 @@
+[
+  {
+    "error": "transaction type not supported"
+  },
+  {
+    "error": "transaction type not supported"
+  }
+]
diff --git a/cmd/evm/testdata/15/exp2.json b/cmd/evm/testdata/15/exp2.json
new file mode 100644
index 0000000000000000000000000000000000000000..85d821f557cb9fac3266950e0522264179b3354e
--- /dev/null
+++ b/cmd/evm/testdata/15/exp2.json
@@ -0,0 +1,10 @@
+[
+  {
+    "address": "0xd02d72e067e77158444ef2020ff2d325f929b363",
+    "hash": "0xa98a24882ea90916c6a86da650fbc6b14238e46f0af04a131ce92be897507476"
+  },
+  {
+    "address": "0xd02d72e067e77158444ef2020ff2d325f929b363",
+    "hash": "0x36bad80acce7040c45fd32764b5c2b2d2e6f778669fb41791f73f546d56e739a"
+  }
+]
diff --git a/cmd/evm/testdata/15/exp3.json b/cmd/evm/testdata/15/exp3.json
new file mode 100644
index 0000000000000000000000000000000000000000..6c46d267cf37035d44c5ec20211219db1c423b6c
--- /dev/null
+++ b/cmd/evm/testdata/15/exp3.json
@@ -0,0 +1,47 @@
+[
+  {
+    "error": "transaction type not supported"
+  },
+  {
+    "error": "transaction type not supported"
+  },
+  {
+    "error": "transaction type not supported"
+  },
+  {
+    "error": "transaction type not supported"
+  },
+  {
+    "error": "transaction type not supported"
+  },
+  {
+    "error": "transaction type not supported"
+  },
+  {
+    "error": "transaction type not supported"
+  },
+  {
+    "error": "rlp: expected List"
+  },
+  {
+    "error": "rlp: expected List"
+  },
+  {
+    "error": "rlp: expected List"
+  },
+  {
+    "error": "rlp: expected List"
+  },
+  {
+    "error": "rlp: expected List"
+  },
+  {
+    "error": "rlp: expected input list for types.AccessListTx"
+  },
+  {
+    "error": "transaction type not supported"
+  },
+  {
+    "error": "transaction type not supported"
+  }
+]
diff --git a/cmd/evm/testdata/15/signed_txs.rlp b/cmd/evm/testdata/15/signed_txs.rlp
new file mode 100644
index 0000000000000000000000000000000000000000..9d1157ea45d9b5d8e469c4b8ef9ee6285b93bc93
--- /dev/null
+++ b/cmd/evm/testdata/15/signed_txs.rlp
@@ -0,0 +1 @@
+"0xf8d2b86702f864010180820fa08284d09411111111111111111111111111111111111111118080c001a0b7dfab36232379bb3d1497a4f91c1966b1f932eae3ade107bf5d723b9cb474e0a06261c359a10f2132f126d250485b90cf20f30340801244a08ef6142ab33d1904b86702f864010280820fa08284d09411111111111111111111111111111111111111118080c080a0d4ec563b6568cd42d998fc4134b36933c6568d01533b5adf08769270243c6c7fa072bf7c21eac6bbeae5143371eef26d5e279637f3bd73482b55979d76d935b1e9"
\ No newline at end of file