diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go
index d6d525eae10da3dbf51b87843082994a9be61215..9de427ae439198e864accacba3bbb2423ce2635a 100644
--- a/accounts/abi/bind/backends/simulated.go
+++ b/accounts/abi/bind/backends/simulated.go
@@ -716,6 +716,8 @@ func (m callMsg) Nonce() uint64                { return 0 }
 func (m callMsg) CheckNonce() bool             { return false }
 func (m callMsg) To() *common.Address          { return m.CallMsg.To }
 func (m callMsg) GasPrice() *big.Int           { return m.CallMsg.GasPrice }
+func (m callMsg) FeeCap() *big.Int             { return m.CallMsg.FeeCap }
+func (m callMsg) Tip() *big.Int                { return m.CallMsg.Tip }
 func (m callMsg) Gas() uint64                  { return m.CallMsg.Gas }
 func (m callMsg) Value() *big.Int              { return m.CallMsg.Value }
 func (m callMsg) Data() []byte                 { return m.CallMsg.Data }
diff --git a/accounts/external/backend.go b/accounts/external/backend.go
index de241385c2d29abbae39dd88c074b0b847433120..59766217d225b278657a5843033fb7f4ec4a9983 100644
--- a/accounts/external/backend.go
+++ b/accounts/external/backend.go
@@ -217,12 +217,12 @@ func (api *ExternalSigner) SignTx(account accounts.Account, tx *types.Transactio
 	if chainID != nil {
 		args.ChainID = (*hexutil.Big)(chainID)
 	}
-	// However, if the user asked for a particular chain id, then we should
-	// use that instead.
-	if tx.Type() != types.LegacyTxType && tx.ChainId() != nil {
-		args.ChainID = (*hexutil.Big)(tx.ChainId())
-	}
-	if tx.Type() == types.AccessListTxType {
+	if tx.Type() != types.LegacyTxType {
+		// However, if the user asked for a particular chain id, then we should
+		// use that instead.
+		if tx.ChainId() != nil {
+			args.ChainID = (*hexutil.Big)(tx.ChainId())
+		}
 		accessList := tx.AccessList()
 		args.AccessList = &accessList
 	}
diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go
index c3f1b16efc9b585ff936cc7350e8d97493c15796..cf6974bc43cbc52528229b7053cb7f5e1168dd90 100644
--- a/cmd/evm/internal/t8ntool/execution.go
+++ b/cmd/evm/internal/t8ntool/execution.go
@@ -69,6 +69,7 @@ type stEnv struct {
 	Timestamp   uint64                              `json:"currentTimestamp"  gencodec:"required"`
 	BlockHashes map[math.HexOrDecimal64]common.Hash `json:"blockHashes,omitempty"`
 	Ommers      []ommer                             `json:"ommers,omitempty"`
+	BaseFee     *big.Int                            `json:"currentBaseFee,omitempty"`
 }
 
 type stEnvMarshaling struct {
@@ -77,6 +78,7 @@ type stEnvMarshaling struct {
 	GasLimit   math.HexOrDecimal64
 	Number     math.HexOrDecimal64
 	Timestamp  math.HexOrDecimal64
+	BaseFee    *math.HexOrDecimal256
 }
 
 // Apply applies a set of transactions to a pre-state
@@ -120,6 +122,10 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
 		GasLimit:    pre.Env.GasLimit,
 		GetHash:     getHash,
 	}
+	// If currentBaseFee is defined, add it to the vmContext.
+	if pre.Env.BaseFee != nil {
+		vmContext.BaseFee = new(big.Int).Set(pre.Env.BaseFee)
+	}
 	// If DAO is supported/enabled, we need to handle it here. In geth 'proper', it's
 	// done in StateProcessor.Process(block, ...), right before transactions are applied.
 	if chainConfig.DAOForkSupport &&
@@ -129,7 +135,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
 	}
 
 	for i, tx := range txs {
-		msg, err := tx.AsMessage(signer)
+		msg, err := tx.AsMessage(signer, pre.Env.BaseFee)
 		if err != nil {
 			log.Info("rejected tx", "index", i, "hash", tx.Hash(), "error", err)
 			rejectedTxs = append(rejectedTxs, i)
diff --git a/cmd/evm/internal/t8ntool/gen_stenv.go b/cmd/evm/internal/t8ntool/gen_stenv.go
index ab5951534e48f9643cce6ef50f4634732bc51998..695fdba1e15d8f71557e24e16097620fb494e9a7 100644
--- a/cmd/evm/internal/t8ntool/gen_stenv.go
+++ b/cmd/evm/internal/t8ntool/gen_stenv.go
@@ -16,13 +16,14 @@ var _ = (*stEnvMarshaling)(nil)
 // MarshalJSON marshals as JSON.
 func (s stEnv) MarshalJSON() ([]byte, error) {
 	type stEnv struct {
-		Coinbase    common.UnprefixedAddress            `json:"currentCoinbase"   gencodec:"required"`
+		Coinbase    common.UnprefixedAddress            `json:"currentCoinbase" gencodec:"required"`
 		Difficulty  *math.HexOrDecimal256               `json:"currentDifficulty" gencodec:"required"`
-		GasLimit    math.HexOrDecimal64                 `json:"currentGasLimit"   gencodec:"required"`
-		Number      math.HexOrDecimal64                 `json:"currentNumber"     gencodec:"required"`
-		Timestamp   math.HexOrDecimal64                 `json:"currentTimestamp"  gencodec:"required"`
+		GasLimit    math.HexOrDecimal64                 `json:"currentGasLimit" gencodec:"required"`
+		Number      math.HexOrDecimal64                 `json:"currentNumber" gencodec:"required"`
+		Timestamp   math.HexOrDecimal64                 `json:"currentTimestamp" gencodec:"required"`
 		BlockHashes map[math.HexOrDecimal64]common.Hash `json:"blockHashes,omitempty"`
 		Ommers      []ommer                             `json:"ommers,omitempty"`
+		BaseFee     *math.HexOrDecimal256               `json:"currentBaseFee,omitempty"`
 	}
 	var enc stEnv
 	enc.Coinbase = common.UnprefixedAddress(s.Coinbase)
@@ -32,19 +33,21 @@ func (s stEnv) MarshalJSON() ([]byte, error) {
 	enc.Timestamp = math.HexOrDecimal64(s.Timestamp)
 	enc.BlockHashes = s.BlockHashes
 	enc.Ommers = s.Ommers
+	enc.BaseFee = (*math.HexOrDecimal256)(s.BaseFee)
 	return json.Marshal(&enc)
 }
 
 // UnmarshalJSON unmarshals from JSON.
 func (s *stEnv) UnmarshalJSON(input []byte) error {
 	type stEnv struct {
-		Coinbase    *common.UnprefixedAddress           `json:"currentCoinbase"   gencodec:"required"`
+		Coinbase    *common.UnprefixedAddress           `json:"currentCoinbase" gencodec:"required"`
 		Difficulty  *math.HexOrDecimal256               `json:"currentDifficulty" gencodec:"required"`
-		GasLimit    *math.HexOrDecimal64                `json:"currentGasLimit"   gencodec:"required"`
-		Number      *math.HexOrDecimal64                `json:"currentNumber"     gencodec:"required"`
-		Timestamp   *math.HexOrDecimal64                `json:"currentTimestamp"  gencodec:"required"`
+		GasLimit    *math.HexOrDecimal64                `json:"currentGasLimit" gencodec:"required"`
+		Number      *math.HexOrDecimal64                `json:"currentNumber" gencodec:"required"`
+		Timestamp   *math.HexOrDecimal64                `json:"currentTimestamp" gencodec:"required"`
 		BlockHashes map[math.HexOrDecimal64]common.Hash `json:"blockHashes,omitempty"`
 		Ommers      []ommer                             `json:"ommers,omitempty"`
+		BaseFee     *math.HexOrDecimal256               `json:"currentBaseFee,omitempty"`
 	}
 	var dec stEnv
 	if err := json.Unmarshal(input, &dec); err != nil {
@@ -76,5 +79,8 @@ func (s *stEnv) UnmarshalJSON(input []byte) error {
 	if dec.Ommers != nil {
 		s.Ommers = dec.Ommers
 	}
+	if dec.BaseFee != nil {
+		s.BaseFee = (*big.Int)(dec.BaseFee)
+	}
 	return nil
 }
diff --git a/cmd/evm/internal/t8ntool/transition.go b/cmd/evm/internal/t8ntool/transition.go
index 9bb03c2c6aaf0b2209132c84056d8b5be6aeb97f..bab6e63faa6afe9143f8b9ca30f39d35c10ea66a 100644
--- a/cmd/evm/internal/t8ntool/transition.go
+++ b/cmd/evm/internal/t8ntool/transition.go
@@ -19,6 +19,7 @@ package t8ntool
 import (
 	"crypto/ecdsa"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"math/big"
@@ -210,9 +211,12 @@ func Main(ctx *cli.Context) error {
 	if txs, err = signUnsignedTransactions(txsWithKeys, signer); err != nil {
 		return NewError(ErrorJson, fmt.Errorf("failed signing transactions: %v", err))
 	}
-
-	// Iterate over all the tests, run them and aggregate the results
-
+	// Sanity check, to not `panic` in state_transition
+	if chainConfig.IsLondon(big.NewInt(int64(prestate.Env.Number))) {
+		if prestate.Env.BaseFee == nil {
+			return NewError(ErrorVMConfig, errors.New("EIP-1559 config but missing 'currentBaseFee' in env section"))
+		}
+	}
 	// Run the test and aggregate the result
 	s, result, err := prestate.Apply(vmConfig, chainConfig, txs, ctx.Int64(RewardFlag.Name), getTracer)
 	if err != nil {
diff --git a/cmd/evm/poststate.json b/cmd/evm/poststate.json
deleted file mode 100644
index 9ee17f18d139858580cf20a592bd21365e1c6bde..0000000000000000000000000000000000000000
--- a/cmd/evm/poststate.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
-    "root": "f4157bb27bcb1d1a63001434a249a80948f2e9fe1f53d551244c1dae826b5b23",
-    "accounts": {
-        "0x8a8eafb1cf62bfbeb1741769dae1a9dd47996192": {
-            "balance": "4276951709",
-            "nonce": 1,
-            "root": "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
-            "codeHash": "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
-        },
-        "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": {
-            "balance": "6916764286133345652",
-            "nonce": 172,
-            "root": "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
-            "codeHash": "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
-        },
-        "0xc94f5374fce5edbc8e2a8697c15331677e6ebf0b": {
-            "balance": "42500",
-            "nonce": 0,
-            "root": "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
-            "codeHash": "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
-        }
-    }
-}
\ No newline at end of file
diff --git a/cmd/evm/testdata/10/alloc.json b/cmd/evm/testdata/10/alloc.json
new file mode 100644
index 0000000000000000000000000000000000000000..6e98e7513c454a5491bf918c010192bc6cd9dc77
--- /dev/null
+++ b/cmd/evm/testdata/10/alloc.json
@@ -0,0 +1,23 @@
+{
+  "0x1111111111111111111111111111111111111111" : {
+    "balance" : "0x010000000000",
+    "code" : "0xfe",
+    "nonce" : "0x01",
+    "storage" : {
+    }
+  },
+  "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" : {
+    "balance" : "0x010000000000",
+    "code" : "0x",
+    "nonce" : "0x01",
+    "storage" : {
+    }
+  },
+  "0xd02d72e067e77158444ef2020ff2d325f929b363" : {
+    "balance" : "0x01000000000000",
+    "code" : "0x",
+    "nonce" : "0x01",
+    "storage" : {
+    }
+  }
+}
\ No newline at end of file
diff --git a/cmd/evm/testdata/10/env.json b/cmd/evm/testdata/10/env.json
new file mode 100644
index 0000000000000000000000000000000000000000..3a82d46a774b49b68c52186878dcb0f567307469
--- /dev/null
+++ b/cmd/evm/testdata/10/env.json
@@ -0,0 +1,12 @@
+{
+  "currentCoinbase" : "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba",
+  "currentDifficulty" : "0x020000",
+  "currentNumber" : "0x01",
+  "currentTimestamp" : "0x079e",
+  "previousHash" : "0xcb23ee65a163121f640673b41788ee94633941405f95009999b502eedfbbfd4f",
+  "currentGasLimit" : "0x40000000",
+  "currentBaseFee" : "0x036b",
+  "blockHashes" : {
+    "0" : "0xcb23ee65a163121f640673b41788ee94633941405f95009999b502eedfbbfd4f"
+  }
+}
\ No newline at end of file
diff --git a/cmd/evm/testdata/10/readme.md b/cmd/evm/testdata/10/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..c34be80bb71c4b7d3e17a0f036e3d01cb53c13e1
--- /dev/null
+++ b/cmd/evm/testdata/10/readme.md
@@ -0,0 +1,79 @@
+## EIP-1559 testing
+
+This test contains testcases for EIP-1559, which were reported by Ori as misbehaving. 
+
+```
+[user@work evm]$ dir=./testdata/10 && ./evm t8n --state.fork=London --input.alloc=$dir/alloc.json --input.txs=$dir/txs.json --input.env=$dir/env.json --output.alloc=stdout --output.result=stdout 2>&1
+INFO [05-09|22:11:59.436] rejected tx                              index=3 hash=db07bf..ede1e8 from=0xd02d72E067e77158444ef2020Ff2d325f929B363 error="gas limit reached"
+```
+Output:
+```json
+{
+ "alloc": {
+  "0x1111111111111111111111111111111111111111": {
+   "code": "0xfe",
+   "balance": "0x10000000000",
+   "nonce": "0x1"
+  },
+  "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": {
+   "balance": "0x10000000000",
+   "nonce": "0x1"
+  },
+  "0xd02d72e067e77158444ef2020ff2d325f929b363": {
+   "balance": "0xff5beffffc95",
+   "nonce": "0x4"
+  }
+ },
+ "result": {
+  "stateRoot": "0xf91a7ec08e4bfea88719aab34deabb000c86902360532b52afa9599d41f2bb8b",
+  "txRoot": "0xda925f2306a52fa24c15d5cd212d736ee016415fd8dd0c45fd368de7917d64bb",
+  "receiptRoot": "0x439a25f7fc424c10fb1f89800e4aa1df74156b137239d9ac3eaa7c911c353cd5",
+  "logsHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
+  "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+  "receipts": [
+   {
+    "type": "0x2",
+    "root": "0x",
+    "status": "0x0",
+    "cumulativeGasUsed": "0x10000001",
+    "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+    "logs": null,
+    "transactionHash": "0x88980f6efcc5358d9c359663e7b9414722d430497637340ea056b076bc206701",
+    "contractAddress": "0x0000000000000000000000000000000000000000",
+    "gasUsed": "0x10000001",
+    "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
+    "transactionIndex": "0x0"
+   },
+   {
+    "type": "0x2",
+    "root": "0x",
+    "status": "0x0",
+    "cumulativeGasUsed": "0x20000001",
+    "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+    "logs": null,
+    "transactionHash": "0xd7bf3886f4e2aef74d525ae072c680f3846f550254401b67cbfda4a233757582",
+    "contractAddress": "0x0000000000000000000000000000000000000000",
+    "gasUsed": "0x10000000",
+    "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
+    "transactionIndex": "0x1"
+   },
+   {
+    "type": "0x2",
+    "root": "0x",
+    "status": "0x0",
+    "cumulativeGasUsed": "0x30000001",
+    "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+    "logs": null,
+    "transactionHash": "0x50308296760f01f1eeec7500e9e73cad67469249b1f59e9a9f55e6625a4923db",
+    "contractAddress": "0x0000000000000000000000000000000000000000",
+    "gasUsed": "0x10000000",
+    "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
+    "transactionIndex": "0x2"
+   }
+  ],
+  "rejected": [
+   3
+  ]
+ }
+}
+```
diff --git a/cmd/evm/testdata/10/txs.json b/cmd/evm/testdata/10/txs.json
new file mode 100644
index 0000000000000000000000000000000000000000..014f9db9af03c57c898740db13b19b338c8c9aab
--- /dev/null
+++ b/cmd/evm/testdata/10/txs.json
@@ -0,0 +1,70 @@
+[
+  {
+    "input" : "0x",
+    "gas" : "0x10000001",
+    "nonce" : "0x1",
+    "to" : "0x1111111111111111111111111111111111111111",
+    "value" : "0x0",
+    "v" : "0x0",
+    "r" : "0x7a45f00bcde9036b026cdf1628b023cd8a31a95c62b5e4dbbee2fa7debe668fb",
+    "s" : "0x3cc9d6f2cd00a045b0263f2d6dad7d60938d5d13d061af4969f95928aa934d4a",
+    "secretKey" : "0x41f6e321b31e72173f8ff2e292359e1862f24fba42fe6f97efaf641980eff298",
+    "chainId" : "0x1",
+    "type" : "0x2",
+    "feeCap" : "0xfa0",
+    "tip" : "0x0",
+    "accessList" : [
+    ]
+  },
+  {
+    "input" : "0x",
+    "gas" : "0x10000000",
+    "nonce" : "0x2",
+    "to" : "0x1111111111111111111111111111111111111111",
+    "value" : "0x0",
+    "v" : "0x0",
+    "r" : "0x4c564b94b0281a8210eeec2dd1fe2e16ff1c1903a8c3a1078d735d7f8208b2af",
+    "s" : "0x56432b2593e6de95db1cb997b7385217aca03f1615327e231734446b39f266d",
+    "secretKey" : "0x41f6e321b31e72173f8ff2e292359e1862f24fba42fe6f97efaf641980eff298",
+    "chainId" : "0x1",
+    "type" : "0x2",
+    "feeCap" : "0xfa0",
+    "tip" : "0x0",
+    "accessList" : [
+    ]
+  },
+  {
+    "input" : "0x",
+    "gas" : "0x10000000",
+    "nonce" : "0x3",
+    "to" : "0x1111111111111111111111111111111111111111",
+    "value" : "0x0",
+    "v" : "0x0",
+    "r" : "0x2ed2ef52f924f59d4a21e1f2a50d3b1109303ce5e32334a7ece9b46f4fbc2a57",
+    "s" : "0x2980257129cbd3da987226f323d50ba3975a834d165e0681f991b75615605c44",
+    "secretKey" : "0x41f6e321b31e72173f8ff2e292359e1862f24fba42fe6f97efaf641980eff298",
+    "chainId" : "0x1",
+    "type" : "0x2",
+    "feeCap" : "0xfa0",
+    "tip" : "0x0",
+    "accessList" : [
+    ]
+  },
+  {
+    "input" : "0x",
+    "gas" : "0x10000000",
+    "nonce" : "0x4",
+    "to" : "0x1111111111111111111111111111111111111111",
+    "value" : "0x0",
+    "v" : "0x0",
+    "r" : "0x5df7d7f8f8e15b36fc9f189cacb625040fad10398d08fc90812595922a2c49b2",
+    "s" : "0x565fc1803f77a84d754ffe3c5363ab54a8d93a06ea1bb9d4c73c73a282b35917",
+    "secretKey" : "0x41f6e321b31e72173f8ff2e292359e1862f24fba42fe6f97efaf641980eff298",
+    "chainId" : "0x1",
+    "type" : "0x2",
+    "feeCap" : "0xfa0",
+    "tip" : "0x0",
+    "accessList" : [
+    ]
+  }
+]
\ No newline at end of file
diff --git a/cmd/evm/testdata/11/alloc.json b/cmd/evm/testdata/11/alloc.json
new file mode 100644
index 0000000000000000000000000000000000000000..86938230fa75dc43891de58d93494f5b60468b05
--- /dev/null
+++ b/cmd/evm/testdata/11/alloc.json
@@ -0,0 +1,25 @@
+{
+    "0x0f572e5295c57f15886f9b263e2f6d2d6c7b5ec6" : {
+        "balance" : "0x0de0b6b3a7640000",
+        "code" : "0x61ffff5060046000f3",
+        "nonce" : "0x01",
+        "storage" : {
+        }
+    },
+    "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" : {
+        "balance" : "0x0de0b6b3a7640000",
+        "code" : "0x",
+        "nonce" : "0x00",
+        "storage" : {
+            "0x00" : "0x00"
+        }
+    },
+    "0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b" : {
+        "balance" : "0x00",
+        "code" : "0x6001600055",
+        "nonce" : "0x00",
+        "storage" : {
+        }
+    }
+}
+
diff --git a/cmd/evm/testdata/11/env.json b/cmd/evm/testdata/11/env.json
new file mode 100644
index 0000000000000000000000000000000000000000..37dedf09475a86ad00fc042f4834464cb9fe2dae
--- /dev/null
+++ b/cmd/evm/testdata/11/env.json
@@ -0,0 +1,12 @@
+{
+    "currentCoinbase" : "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba",
+    "currentDifficulty" : "0x020000",
+    "currentNumber" : "0x01",
+    "currentTimestamp" : "0x03e8",
+    "previousHash" : "0xfda4419b3660e99f37e536dae1ab081c180136bb38c837a93e93d9aab58553b2",
+    "currentGasLimit" : "0x0f4240",
+    "blockHashes" : {
+        "0" : "0xfda4419b3660e99f37e536dae1ab081c180136bb38c837a93e93d9aab58553b2"
+    }
+}
+
diff --git a/cmd/evm/testdata/11/readme.md b/cmd/evm/testdata/11/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..d499f8e99fae848e6b06f54a552cb67f560eea23
--- /dev/null
+++ b/cmd/evm/testdata/11/readme.md
@@ -0,0 +1,13 @@
+## Test missing basefee
+
+In this test, the `currentBaseFee` is missing from the env portion. 
+On a live blockchain, the basefee is present in the header, and verified as part of header validation. 
+
+In `evm t8n`, we don't have blocks, so it needs to be added in the `env`instead. 
+
+When it's missing, an error is expected. 
+
+```
+dir=./testdata/11 && ./evm t8n --state.fork=London --input.alloc=$dir/alloc.json --input.txs=$dir/txs.json --input.env=$dir/env.json --output.alloc=stdout --output.result=stdout 2>&1>/dev/null
+ERROR(3): EIP-1559 config but missing 'currentBaseFee' in env section
+```
\ No newline at end of file
diff --git a/cmd/evm/testdata/11/txs.json b/cmd/evm/testdata/11/txs.json
new file mode 100644
index 0000000000000000000000000000000000000000..c54b0a1f5b4d78eaff81f2225988478d072d4d94
--- /dev/null
+++ b/cmd/evm/testdata/11/txs.json
@@ -0,0 +1,14 @@
+[
+    {
+        "input" : "0x38600060013960015160005560006000f3",
+        "gas" : "0x61a80",
+        "gasPrice" : "0x1",
+        "nonce" : "0x0",
+        "value" : "0x186a0",
+        "v" : "0x1c",
+        "r" : "0x2e1391fd903387f1cc2b51df083805fb4bbb0d4710a2cdf4a044d191ff7be63e",
+        "s" : "0x7f10a933c42ab74927db02b1db009e923d9d2ab24ac24d63c399f2fe5d9c9b22",
+        "secretKey" : "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8"
+    }
+]
+
diff --git a/cmd/evm/testdata/9/alloc.json b/cmd/evm/testdata/9/alloc.json
new file mode 100644
index 0000000000000000000000000000000000000000..c14e38e845156e055db5c8cc2337b4e2695b3ad3
--- /dev/null
+++ b/cmd/evm/testdata/9/alloc.json
@@ -0,0 +1,11 @@
+{
+  "0x000000000000000000000000000000000000aaaa": {
+    "balance": "0x03",
+    "code": "0x58585454",
+    "nonce": "0x1"
+  },
+  "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": {
+    "balance": "0x100000000000000",
+    "nonce": "0x00"
+  }
+}
diff --git a/cmd/evm/testdata/9/env.json b/cmd/evm/testdata/9/env.json
new file mode 100644
index 0000000000000000000000000000000000000000..ec5164b9952eafcf55f76ee0b4c7ef6bdd50d60c
--- /dev/null
+++ b/cmd/evm/testdata/9/env.json
@@ -0,0 +1,8 @@
+{
+  "currentCoinbase": "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba",
+  "currentDifficulty": "0x20000",
+  "currentGasTarget": "0x1000000000",
+  "currentBaseFee": "0x3B9ACA00",
+  "currentNumber": "0x1000000",
+  "currentTimestamp": "0x04"
+}
diff --git a/cmd/evm/testdata/9/readme.md b/cmd/evm/testdata/9/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..88f0f12aaaa5731f0b873c5d7ab4a064457d0358
--- /dev/null
+++ b/cmd/evm/testdata/9/readme.md
@@ -0,0 +1,75 @@
+## EIP-1559 testing
+
+This test contains testcases for EIP-1559, which uses an new transaction type and has a new block parameter. 
+
+### Prestate
+
+The alloc portion contains one contract (`0x000000000000000000000000000000000000aaaa`), containing the 
+following code: `0x58585454`: `PC; PC; SLOAD; SLOAD`.
+
+Essentialy, this contract does `SLOAD(0)` and `SLOAD(1)`.
+
+The alloc also contains some funds on `0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b`. 
+
+## Transactions
+
+There are two transactions, each invokes the contract above. 
+
+1. EIP-1559 ACL-transaction, which contains the `0x0` slot for `0xaaaa`
+2. Legacy transaction
+
+## Execution 
+
+Running it yields: 
+```
+$ dir=./testdata/9 && ./evm t8n --state.fork=London --input.alloc=$dir/alloc.json --input.txs=$dir/txs.json --input.env=$dir/env.json --trace && cat trace-* | grep SLOAD
+{"pc":2,"op":84,"gas":"0x48c28","gasCost":"0x834","memory":"0x","memSize":0,"stack":["0x0","0x1"],"returnStack":null,"returnD
+ata":"0x","depth":1,"refund":0,"opName":"SLOAD","error":""}
+{"pc":3,"op":84,"gas":"0x483f4","gasCost":"0x64","memory":"0x","memSize":0,"stack":["0x0","0x0"],"returnStack":null,"returnDa
+ta":"0x","depth":1,"refund":0,"opName":"SLOAD","error":""}
+{"pc":2,"op":84,"gas":"0x49cf4","gasCost":"0x834","memory":"0x","memSize":0,"stack":["0x0","0x1"],"returnStack":null,"returnD
+ata":"0x","depth":1,"refund":0,"opName":"SLOAD","error":""}
+{"pc":3,"op":84,"gas":"0x494c0","gasCost":"0x834","memory":"0x","memSize":0,"stack":["0x0","0x0"],"returnStack":null,"returnD
+ata":"0x","depth":1,"refund":0,"opName":"SLOAD","error":""}
+```
+
+We can also get the post-alloc:
+```
+$ dir=./testdata/9 && ./evm t8n --state.fork=London --input.alloc=$dir/alloc.json --input.txs=$dir/txs.json --input.env=$dir/env.json --output.alloc=stdout
+{
+ "alloc": {
+  "0x000000000000000000000000000000000000aaaa": {
+   "code": "0x58585454",
+   "balance": "0x3",
+   "nonce": "0x1"
+  },
+  "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba": {
+   "balance": "0xbfc02677a000"
+  },
+  "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": {
+   "balance": "0xff104fcfea7800",
+   "nonce": "0x2"
+  }
+ }
+}
+```
+
+If we try to execute it on older rules: 
+```
+dir=./testdata/9 && ./evm t8n --state.fork=Berlin --input.alloc=$dir/alloc.json --input.txs=$dir/txs.json --input.env=$dir/env.json --output.alloc=stdout
+ERROR(10): Failed signing transactions: ERROR(10): Tx 0: failed to sign tx: transaction type not supported
+```
+
+It fails, due to the `evm t8n` cannot sign them in with the given signer. We can bypass that, however, 
+by feeding it presigned transactions, located in `txs_signed.json`. 
+
+```
+dir=./testdata/9 && ./evm t8n --state.fork=Berlin --input.alloc=$dir/alloc.json --input.txs=$dir/txs_signed.json --input.env=$dir/env.json 
+INFO [05-07|12:28:42.072] rejected tx                              index=0 hash=b4821e..536819 error="transaction type not supported"
+INFO [05-07|12:28:42.072] rejected tx                              index=1 hash=a9c6c6..fa4036 from=0xa94f5374Fce5edBC8E2a8697C15331677e6EbF0B error="nonce too high: address 0xa94f5374Fce5edBC8E2a8697C15331677e6EbF0B, tx: 1 state: 0"
+INFO [05-07|12:28:42.073] Wrote file                               file=alloc.json
+INFO [05-07|12:28:42.073] Wrote file                               file=result.json
+```
+
+Number `0` is not applicable, and therefore number `1` has wrong nonce, and both are rejected.
+
diff --git a/cmd/evm/testdata/9/txs.json b/cmd/evm/testdata/9/txs.json
new file mode 100644
index 0000000000000000000000000000000000000000..f349ae4a24a2ac8a16ff9466caa06c812599f085
--- /dev/null
+++ b/cmd/evm/testdata/9/txs.json
@@ -0,0 +1,37 @@
+[
+  {
+    "gas": "0x4ef00",
+    "tip": "0x2",
+    "feeCap": "0x12A05F200",
+    "chainId": "0x1",
+    "input": "0x",
+    "nonce": "0x0",
+    "to": "0x000000000000000000000000000000000000aaaa",
+    "value": "0x0",
+    "type" : "0x2",
+    "accessList": [
+      {"address": "0x000000000000000000000000000000000000aaaa",
+        "storageKeys": [
+          "0x0000000000000000000000000000000000000000000000000000000000000000"
+        ]
+      }
+    ],
+    "v": "0x0",
+    "r": "0x0",
+    "s": "0x0",
+    "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8"
+  },
+  {
+    "gas": "0x4ef00",
+    "gasPrice": "0x12A05F200",
+    "chainId": "0x1",
+    "input": "0x",
+    "nonce": "0x1",
+    "to": "0x000000000000000000000000000000000000aaaa",
+    "value": "0x0",
+    "v": "0x0",
+    "r": "0x0",
+    "s": "0x0",
+    "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8"
+  }
+]
diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go
index 9954a023e574380e18fc33c65c06593dc392216c..b693e8051826490d7fe8cb27f0ebe1b59fe7a084 100644
--- a/consensus/clique/clique.go
+++ b/consensus/clique/clique.go
@@ -299,10 +299,6 @@ func (c *Clique) verifyHeader(chain consensus.ChainHeaderReader, header *types.H
 	if header.GasLimit > cap {
 		return fmt.Errorf("invalid gasLimit: have %v, max %v", header.GasLimit, cap)
 	}
-	// Verify that the gasUsed is <= gasLimit
-	if header.GasUsed > header.GasLimit {
-		return fmt.Errorf("invalid gasUsed: have %d, gasLimit %d", header.GasUsed, header.GasLimit)
-	}
 	// If all checks passed, validate any special fields for hard forks
 	if err := misc.VerifyForkHashes(chain.Config(), header, false); err != nil {
 		return err
@@ -334,14 +330,21 @@ func (c *Clique) verifyCascadingFields(chain consensus.ChainHeaderReader, header
 	if parent.Time+c.config.Period > header.Time {
 		return errInvalidTimestamp
 	}
-	// Verify that the gas limit remains within allowed bounds
-	diff := int64(parent.GasLimit) - int64(header.GasLimit)
-	if diff < 0 {
-		diff *= -1
+	// Verify that the gasUsed is <= gasLimit
+	if header.GasUsed > header.GasLimit {
+		return fmt.Errorf("invalid gasUsed: have %d, gasLimit %d", header.GasUsed, header.GasLimit)
 	}
-	limit := parent.GasLimit / params.GasLimitBoundDivisor
-	if uint64(diff) >= limit || header.GasLimit < params.MinGasLimit {
-		return fmt.Errorf("invalid gas limit: have %d, want %d += %d", header.GasLimit, parent.GasLimit, limit)
+	if !chain.Config().IsLondon(header.Number) {
+		// Verify BaseFee not present before EIP-1559 fork.
+		if header.BaseFee != nil {
+			return fmt.Errorf("invalid baseFee before fork: have %d, want <nil>", header.BaseFee)
+		}
+		if err := misc.VerifyGaslimit(parent.GasLimit, header.GasLimit); err != nil {
+			return err
+		}
+	} else if err := misc.VerifyEip1559Header(chain.Config(), parent, header); err != nil {
+		// Verify the header's EIP-1559 attributes.
+		return err
 	}
 	// Retrieve the snapshot needed to verify this header and cache it
 	snap, err := c.snapshot(chain, number-1, header.ParentHash, parents)
@@ -725,7 +728,7 @@ func CliqueRLP(header *types.Header) []byte {
 }
 
 func encodeSigHeader(w io.Writer, header *types.Header) {
-	err := rlp.Encode(w, []interface{}{
+	enc := []interface{}{
 		header.ParentHash,
 		header.UncleHash,
 		header.Coinbase,
@@ -741,8 +744,11 @@ func encodeSigHeader(w io.Writer, header *types.Header) {
 		header.Extra[:len(header.Extra)-crypto.SignatureLength], // Yes, this will panic if extra is too short
 		header.MixDigest,
 		header.Nonce,
-	})
-	if err != nil {
+	}
+	if header.BaseFee != nil {
+		enc = append(enc, header.BaseFee)
+	}
+	if err := rlp.Encode(w, enc); err != nil {
 		panic("can't encode: " + err.Error())
 	}
 }
diff --git a/consensus/ethash/consensus.go b/consensus/ethash/consensus.go
index b04cb24fb4782c113861c18953adef470a78d722..9b9657e190093726134348289a4ff48a291f4ccf 100644
--- a/consensus/ethash/consensus.go
+++ b/consensus/ethash/consensus.go
@@ -284,16 +284,18 @@ func (ethash *Ethash) verifyHeader(chain consensus.ChainHeaderReader, header, pa
 	if header.GasUsed > header.GasLimit {
 		return fmt.Errorf("invalid gasUsed: have %d, gasLimit %d", header.GasUsed, header.GasLimit)
 	}
-
-	// Verify that the gas limit remains within allowed bounds
-	diff := int64(parent.GasLimit) - int64(header.GasLimit)
-	if diff < 0 {
-		diff *= -1
-	}
-	limit := parent.GasLimit / params.GasLimitBoundDivisor
-
-	if uint64(diff) >= limit || header.GasLimit < params.MinGasLimit {
-		return fmt.Errorf("invalid gas limit: have %d, want %d += %d", header.GasLimit, parent.GasLimit, limit)
+	// Verify the block's gas usage and (if applicable) verify the base fee.
+	if !chain.Config().IsLondon(header.Number) {
+		// Verify BaseFee not present before EIP-1559 fork.
+		if header.BaseFee != nil {
+			return fmt.Errorf("invalid baseFee before fork: have %d, expected 'nil'", header.BaseFee)
+		}
+		if err := misc.VerifyGaslimit(parent.GasLimit, header.GasLimit); err != nil {
+			return err
+		}
+	} else if err := misc.VerifyEip1559Header(chain.Config(), parent, header); err != nil {
+		// Verify the header's EIP-1559 attributes.
+		return err
 	}
 	// Verify that the block number is parent's +1
 	if diff := new(big.Int).Sub(header.Number, parent.Number); diff.Cmp(big.NewInt(1)) != 0 {
@@ -604,7 +606,7 @@ func (ethash *Ethash) FinalizeAndAssemble(chain consensus.ChainHeaderReader, hea
 func (ethash *Ethash) SealHash(header *types.Header) (hash common.Hash) {
 	hasher := sha3.NewLegacyKeccak256()
 
-	rlp.Encode(hasher, []interface{}{
+	enc := []interface{}{
 		header.ParentHash,
 		header.UncleHash,
 		header.Coinbase,
@@ -618,7 +620,11 @@ func (ethash *Ethash) SealHash(header *types.Header) (hash common.Hash) {
 		header.GasUsed,
 		header.Time,
 		header.Extra,
-	})
+	}
+	if header.BaseFee != nil {
+		enc = append(enc, header.BaseFee)
+	}
+	rlp.Encode(hasher, enc)
 	hasher.Sum(hash[:0])
 	return hash
 }
diff --git a/consensus/misc/eip1559.go b/consensus/misc/eip1559.go
new file mode 100644
index 0000000000000000000000000000000000000000..8fca0fdc7092d96b8b1a62f5cced6d01eeeae124
--- /dev/null
+++ b/consensus/misc/eip1559.go
@@ -0,0 +1,93 @@
+// Copyright 2021 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package misc
+
+import (
+	"fmt"
+	"math/big"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/common/math"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/params"
+)
+
+// VerifyEip1559Header verifies some header attributes which were changed in EIP-1559,
+// - gas limit check
+// - basefee check
+func VerifyEip1559Header(config *params.ChainConfig, parent, header *types.Header) error {
+	// Verify that the gas limit remains within allowed bounds
+	parentGasLimit := parent.GasLimit
+	if !config.IsLondon(parent.Number) {
+		parentGasLimit = parent.GasLimit * params.ElasticityMultiplier
+	}
+	if err := VerifyGaslimit(parentGasLimit, header.GasLimit); err != nil {
+		return err
+	}
+	// Verify the header is not malformed
+	if header.BaseFee == nil {
+		return fmt.Errorf("header is missing baseFee")
+	}
+	// Verify the baseFee is correct based on the parent header.
+	expectedBaseFee := CalcBaseFee(config, parent)
+	if header.BaseFee.Cmp(expectedBaseFee) != 0 {
+		return fmt.Errorf("invalid baseFee: have %s, want %s, parentBaseFee %s, parentGasUsed %d",
+			expectedBaseFee, header.BaseFee, parent.BaseFee, parent.GasUsed)
+	}
+	return nil
+}
+
+// CalcBaseFee calculates the basefee of the header.
+func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int {
+	// If the current block is the first EIP-1559 block, return the InitialBaseFee.
+	if !config.IsLondon(parent.Number) {
+		return new(big.Int).SetUint64(params.InitialBaseFee)
+	}
+
+	var (
+		parentGasTarget          = parent.GasLimit / params.ElasticityMultiplier
+		parentGasTargetBig       = new(big.Int).SetUint64(parentGasTarget)
+		baseFeeChangeDenominator = new(big.Int).SetUint64(params.BaseFeeChangeDenominator)
+	)
+	// If the parent gasUsed is the same as the target, the baseFee remains unchanged.
+	if parent.GasUsed == parentGasTarget {
+		return new(big.Int).Set(parent.BaseFee)
+	}
+	if parent.GasUsed > parentGasTarget {
+		// If the parent block used more gas than its target, the baseFee should increase.
+		gasUsedDelta := new(big.Int).SetUint64(parent.GasUsed - parentGasTarget)
+		x := new(big.Int).Mul(parent.BaseFee, gasUsedDelta)
+		y := x.Div(x, parentGasTargetBig)
+		baseFeeDelta := math.BigMax(
+			x.Div(y, baseFeeChangeDenominator),
+			common.Big1,
+		)
+
+		return x.Add(parent.BaseFee, baseFeeDelta)
+	} else {
+		// Otherwise if the parent block used less gas than its target, the baseFee should decrease.
+		gasUsedDelta := new(big.Int).SetUint64(parentGasTarget - parent.GasUsed)
+		x := new(big.Int).Mul(parent.BaseFee, gasUsedDelta)
+		y := x.Div(x, parentGasTargetBig)
+		baseFeeDelta := x.Div(y, baseFeeChangeDenominator)
+
+		return math.BigMax(
+			x.Sub(parent.BaseFee, baseFeeDelta),
+			common.Big0,
+		)
+	}
+}
diff --git a/consensus/misc/eip1559_test.go b/consensus/misc/eip1559_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..333411db514c806053808493dbc7ae30e8fa8fa8
--- /dev/null
+++ b/consensus/misc/eip1559_test.go
@@ -0,0 +1,133 @@
+// Copyright 2021 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package misc
+
+import (
+	"math/big"
+	"testing"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/params"
+)
+
+// copyConfig does a _shallow_ copy of a given config. Safe to set new values, but
+// do not use e.g. SetInt() on the numbers. For testing only
+func copyConfig(original *params.ChainConfig) *params.ChainConfig {
+	return &params.ChainConfig{
+		ChainID:             original.ChainID,
+		HomesteadBlock:      original.HomesteadBlock,
+		DAOForkBlock:        original.DAOForkBlock,
+		DAOForkSupport:      original.DAOForkSupport,
+		EIP150Block:         original.EIP150Block,
+		EIP150Hash:          original.EIP150Hash,
+		EIP155Block:         original.EIP155Block,
+		EIP158Block:         original.EIP158Block,
+		ByzantiumBlock:      original.ByzantiumBlock,
+		ConstantinopleBlock: original.ConstantinopleBlock,
+		PetersburgBlock:     original.PetersburgBlock,
+		IstanbulBlock:       original.IstanbulBlock,
+		MuirGlacierBlock:    original.MuirGlacierBlock,
+		BerlinBlock:         original.BerlinBlock,
+		LondonBlock:         original.LondonBlock,
+		EWASMBlock:          original.EWASMBlock,
+		CatalystBlock:       original.CatalystBlock,
+		Ethash:              original.Ethash,
+		Clique:              original.Clique,
+	}
+}
+
+func config() *params.ChainConfig {
+	config := copyConfig(params.TestChainConfig)
+	config.LondonBlock = big.NewInt(5)
+	return config
+}
+
+// TestBlockGasLimits tests the gasLimit checks for blocks both across
+// the EIP-1559 boundary and post-1559 blocks
+func TestBlockGasLimits(t *testing.T) {
+	initial := new(big.Int).SetUint64(params.InitialBaseFee)
+
+	for i, tc := range []struct {
+		pGasLimit uint64
+		pNum      int64
+		gasLimit  uint64
+		ok        bool
+	}{
+		// Transitions from non-london to london
+		{10000000, 4, 20000000, true},  // No change
+		{10000000, 4, 20019530, true},  // Upper limit
+		{10000000, 4, 20019531, false}, // Upper +1
+		{10000000, 4, 19980470, true},  // Lower limit
+		{10000000, 4, 19980469, false}, // Lower limit -1
+		// London to London
+		{20000000, 5, 20000000, true},
+		{20000000, 5, 20019530, true},  // Upper limit
+		{20000000, 5, 20019531, false}, // Upper limit +1
+		{20000000, 5, 19980470, true},  // Lower limit
+		{20000000, 5, 19980469, false}, // Lower limit -1
+		{40000000, 5, 40039061, true},  // Upper limit
+		{40000000, 5, 40039062, false}, // Upper limit +1
+		{40000000, 5, 39960939, true},  // lower limit
+		{40000000, 5, 39960938, false}, // Lower limit -1
+	} {
+		parent := &types.Header{
+			GasUsed:  tc.pGasLimit / 2,
+			GasLimit: tc.pGasLimit,
+			BaseFee:  initial,
+			Number:   big.NewInt(tc.pNum),
+		}
+		header := &types.Header{
+			GasUsed:  tc.gasLimit / 2,
+			GasLimit: tc.gasLimit,
+			BaseFee:  initial,
+			Number:   big.NewInt(tc.pNum + 1),
+		}
+		err := VerifyEip1559Header(config(), parent, header)
+		if tc.ok && err != nil {
+			t.Errorf("test %d: Expected valid header: %s", i, err)
+		}
+		if !tc.ok && err == nil {
+			t.Errorf("test %d: Expected invalid header", i)
+		}
+	}
+}
+
+// TestCalcBaseFee assumes all blocks are 1559-blocks
+func TestCalcBaseFee(t *testing.T) {
+	tests := []struct {
+		parentBaseFee   int64
+		parentGasLimit  uint64
+		parentGasUsed   uint64
+		expectedBaseFee int64
+	}{
+		{params.InitialBaseFee, 20000000, 10000000, params.InitialBaseFee}, // usage == target
+		{params.InitialBaseFee, 20000000, 9000000, 987500000},              // usage below target
+		{params.InitialBaseFee, 20000000, 11000000, 1012500000},            // usage above target
+	}
+	for i, test := range tests {
+		parent := &types.Header{
+			Number:   common.Big32,
+			GasLimit: test.parentGasLimit,
+			GasUsed:  test.parentGasUsed,
+			BaseFee:  big.NewInt(test.parentBaseFee),
+		}
+		if have, want := CalcBaseFee(config(), parent), big.NewInt(test.expectedBaseFee); have.Cmp(want) != 0 {
+			t.Errorf("test %d: have %d  want %d, ", i, have, want)
+		}
+	}
+}
diff --git a/consensus/misc/gaslimit.go b/consensus/misc/gaslimit.go
new file mode 100644
index 0000000000000000000000000000000000000000..25f35300b94d7b8413b3be47dbb13fc0fb0ba4b1
--- /dev/null
+++ b/consensus/misc/gaslimit.go
@@ -0,0 +1,42 @@
+// Copyright 2021 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package misc
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/ethereum/go-ethereum/params"
+)
+
+// VerifyGaslimit verifies the header gas limit according increase/decrease
+// in relation to the parent gas limit.
+func VerifyGaslimit(parentGasLimit, headerGasLimit uint64) error {
+	// Verify that the gas limit remains within allowed bounds
+	diff := int64(parentGasLimit) - int64(headerGasLimit)
+	if diff < 0 {
+		diff *= -1
+	}
+	limit := parentGasLimit / params.GasLimitBoundDivisor
+	if uint64(diff) >= limit {
+		return fmt.Errorf("invalid gas limit: have %d, want %d +-= %d", headerGasLimit, parentGasLimit, limit-1)
+	}
+	if headerGasLimit < params.MinGasLimit {
+		return errors.New("invalid gas limit below 5000")
+	}
+	return nil
+}
diff --git a/core/blockchain_test.go b/core/blockchain_test.go
index a8ce4c7b9a6473f36ac2a46ecb709193f72844d5..8ace9f6a6d42c5ae2984abfa79db38614b7561e4 100644
--- a/core/blockchain_test.go
+++ b/core/blockchain_test.go
@@ -73,6 +73,10 @@ func newCanonical(engine consensus.Engine, n int, full bool) (ethdb.Database, *B
 	return db, blockchain, err
 }
 
+func newGwei(n int64) *big.Int {
+	return new(big.Int).Mul(big.NewInt(n), big.NewInt(params.GWei))
+}
+
 // Test fork of length N starting from block i
 func testFork(t *testing.T, blockchain *BlockChain, i, n int, full bool, comparator func(td1, td2 *big.Int)) {
 	// Copy old chain up to #i into a new db
@@ -3109,3 +3113,156 @@ func TestEIP2718Transition(t *testing.T) {
 
 	}
 }
+
+// TestEIP1559Transition tests the following:
+//
+// 1. A tranaction whose feeCap is greater than the baseFee is valid.
+// 2. Gas accounting for access lists on EIP-1559 transactions is correct.
+// 3. Only the transaction's tip will be received by the coinbase.
+// 4. The transaction sender pays for both the tip and baseFee.
+// 5. The coinbase receives only the partially realized tip when
+//    feeCap - tip < baseFee.
+// 6. Legacy transaction behave as expected (e.g. gasPrice = feeCap = tip).
+func TestEIP1559Transition(t *testing.T) {
+	var (
+		aa = common.HexToAddress("0x000000000000000000000000000000000000aaaa")
+
+		// Generate a canonical chain to act as the main dataset
+		engine = ethash.NewFaker()
+		db     = rawdb.NewMemoryDatabase()
+
+		// A sender who makes transactions, has some funds
+		key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+		key2, _ = crypto.HexToECDSA("8a1f9a8f95be41cd7ccb6168179afb4504aefe388d1e14474d32c45c72ce7b7a")
+		addr1   = crypto.PubkeyToAddress(key1.PublicKey)
+		addr2   = crypto.PubkeyToAddress(key2.PublicKey)
+		funds   = new(big.Int).Mul(common.Big1, big.NewInt(params.Ether))
+		gspec   = &Genesis{
+			Config: params.AllEthashProtocolChanges,
+			Alloc: GenesisAlloc{
+				addr1: {Balance: funds},
+				addr2: {Balance: funds},
+				// The address 0xAAAA sloads 0x00 and 0x01
+				aa: {
+					Code: []byte{
+						byte(vm.PC),
+						byte(vm.PC),
+						byte(vm.SLOAD),
+						byte(vm.SLOAD),
+					},
+					Nonce:   0,
+					Balance: big.NewInt(0),
+				},
+			},
+		}
+	)
+
+	gspec.Config.BerlinBlock = common.Big0
+	gspec.Config.LondonBlock = common.Big0
+	genesis := gspec.MustCommit(db)
+	signer := types.LatestSigner(gspec.Config)
+
+	blocks, _ := GenerateChain(gspec.Config, genesis, engine, db, 1, func(i int, b *BlockGen) {
+		b.SetCoinbase(common.Address{1})
+
+		// One transaction to 0xAAAA
+		accesses := types.AccessList{types.AccessTuple{
+			Address:     aa,
+			StorageKeys: []common.Hash{{0}},
+		}}
+
+		txdata := &types.DynamicFeeTx{
+			ChainID:    gspec.Config.ChainID,
+			Nonce:      0,
+			To:         &aa,
+			Gas:        30000,
+			FeeCap:     newGwei(5),
+			Tip:        big.NewInt(2),
+			AccessList: accesses,
+			Data:       []byte{},
+		}
+		tx := types.NewTx(txdata)
+		tx, _ = types.SignTx(tx, signer, key1)
+
+		b.AddTx(tx)
+	})
+
+	diskdb := rawdb.NewMemoryDatabase()
+	gspec.MustCommit(diskdb)
+
+	chain, err := NewBlockChain(diskdb, nil, gspec.Config, engine, vm.Config{}, nil, nil)
+	if err != nil {
+		t.Fatalf("failed to create tester chain: %v", err)
+	}
+	if n, err := chain.InsertChain(blocks); err != nil {
+		t.Fatalf("block %d: failed to insert into chain: %v", n, err)
+	}
+
+	block := chain.GetBlockByNumber(1)
+
+	// 1+2: Ensure EIP-1559 access lists are accounted for via gas usage.
+	expectedGas := params.TxGas + params.TxAccessListAddressGas + params.TxAccessListStorageKeyGas +
+		vm.GasQuickStep*2 + params.WarmStorageReadCostEIP2929 + params.ColdSloadCostEIP2929
+	if block.GasUsed() != expectedGas {
+		t.Fatalf("incorrect amount of gas spent: expected %d, got %d", expectedGas, block.GasUsed())
+	}
+
+	state, _ := chain.State()
+
+	// 3: Ensure that miner received only the tx's tip.
+	actual := state.GetBalance(block.Coinbase())
+	expected := new(big.Int).Add(
+		new(big.Int).SetUint64(block.GasUsed()*block.Transactions()[0].Tip().Uint64()),
+		ethash.ConstantinopleBlockReward,
+	)
+	if actual.Cmp(expected) != 0 {
+		t.Fatalf("miner balance incorrect: expected %d, got %d", expected, actual)
+	}
+
+	// 4: Ensure the tx sender paid for the gasUsed * (tip + block baseFee).
+	actual = new(big.Int).Sub(funds, state.GetBalance(addr1))
+	expected = new(big.Int).SetUint64(block.GasUsed() * (block.Transactions()[0].Tip().Uint64() + block.BaseFee().Uint64()))
+	if actual.Cmp(expected) != 0 {
+		t.Fatalf("sender balance incorrect: expected %d, got %d", expected, actual)
+	}
+
+	blocks, _ = GenerateChain(gspec.Config, block, engine, db, 1, func(i int, b *BlockGen) {
+		b.SetCoinbase(common.Address{2})
+
+		txdata := &types.LegacyTx{
+			Nonce:    0,
+			To:       &aa,
+			Gas:      30000,
+			GasPrice: newGwei(5),
+		}
+		tx := types.NewTx(txdata)
+		tx, _ = types.SignTx(tx, signer, key2)
+
+		b.AddTx(tx)
+	})
+
+	if n, err := chain.InsertChain(blocks); err != nil {
+		t.Fatalf("block %d: failed to insert into chain: %v", n, err)
+	}
+
+	block = chain.GetBlockByNumber(2)
+	state, _ = chain.State()
+	effectiveTip := block.Transactions()[0].Tip().Uint64() - block.BaseFee().Uint64()
+
+	// 6+5: Ensure that miner received only the tx's effective tip.
+	actual = state.GetBalance(block.Coinbase())
+	expected = new(big.Int).Add(
+		new(big.Int).SetUint64(block.GasUsed()*effectiveTip),
+		ethash.ConstantinopleBlockReward,
+	)
+	if actual.Cmp(expected) != 0 {
+		t.Fatalf("miner balance incorrect: expected %d, got %d", expected, actual)
+	}
+
+	// 4: Ensure the tx sender paid for the gasUsed * (effectiveTip + block baseFee).
+	actual = new(big.Int).Sub(funds, state.GetBalance(addr2))
+	expected = new(big.Int).SetUint64(block.GasUsed() * (effectiveTip + block.BaseFee().Uint64()))
+	if actual.Cmp(expected) != 0 {
+		t.Fatalf("sender balance incorrect: expected %d, got %d", expected, actual)
+	}
+}
diff --git a/core/chain_makers.go b/core/chain_makers.go
index e058e5a78e5beabd8338ac4174b8d4be47fea2e3..b1b7dc3591df75943a3c9502a64eeaa194f5847b 100644
--- a/core/chain_makers.go
+++ b/core/chain_makers.go
@@ -253,7 +253,7 @@ func makeHeader(chain consensus.ChainReader, parent *types.Block, state *state.S
 		time = parent.Time() + 10 // block time is fixed at 10 seconds
 	}
 
-	return &types.Header{
+	header := &types.Header{
 		Root:       state.IntermediateRoot(chain.Config().IsEIP158(parent.Number())),
 		ParentHash: parent.Hash(),
 		Coinbase:   parent.Coinbase(),
@@ -267,6 +267,12 @@ func makeHeader(chain consensus.ChainReader, parent *types.Block, state *state.S
 		Number:   new(big.Int).Add(parent.Number(), common.Big1),
 		Time:     time,
 	}
+
+	if chain.Config().IsLondon(parent.Number()) {
+		header.BaseFee = misc.CalcBaseFee(chain.Config(), parent.Header())
+	}
+
+	return header
 }
 
 // makeHeaderChain creates a deterministic chain of headers rooted at parent.
diff --git a/core/error.go b/core/error.go
index 197dd81567c1fcbc5bfec6cb018aae2360712d20..3d62cb9bcfaf814dc1f86b6a837ae65a071bd761 100644
--- a/core/error.go
+++ b/core/error.go
@@ -71,4 +71,8 @@ var (
 	// ErrTxTypeNotSupported is returned if a transaction is not supported in the
 	// current network configuration.
 	ErrTxTypeNotSupported = types.ErrTxTypeNotSupported
+
+	// ErrFeeCapTooLow is returned if the transaction fee cap is less than the
+	// the base fee of the block.
+	ErrFeeCapTooLow = errors.New("fee cap less than block base fee")
 )
diff --git a/core/evm.go b/core/evm.go
index 8f69d514995e07100f263ac991ceda265cd5f7fb..6c67fc43762ce92e6b6df8177e0c3cf8325ee20a 100644
--- a/core/evm.go
+++ b/core/evm.go
@@ -37,13 +37,20 @@ type ChainContext interface {
 
 // NewEVMBlockContext creates a new context for use in the EVM.
 func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common.Address) vm.BlockContext {
+	var (
+		beneficiary common.Address
+		baseFee     *big.Int
+	)
+
 	// If we don't have an explicit author (i.e. not mining), extract from the header
-	var beneficiary common.Address
 	if author == nil {
 		beneficiary, _ = chain.Engine().Author(header) // Ignore error, we're past header validation
 	} else {
 		beneficiary = *author
 	}
+	if header.BaseFee != nil {
+		baseFee = new(big.Int).Set(header.BaseFee)
+	}
 	return vm.BlockContext{
 		CanTransfer: CanTransfer,
 		Transfer:    Transfer,
@@ -52,6 +59,7 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common
 		BlockNumber: new(big.Int).Set(header.Number),
 		Time:        new(big.Int).SetUint64(header.Time),
 		Difficulty:  new(big.Int).Set(header.Difficulty),
+		BaseFee:     baseFee,
 		GasLimit:    header.GasLimit,
 	}
 }
diff --git a/core/genesis.go b/core/genesis.go
index d7d08e090994879a220a224ce35613c9eb792eac..b68ae4ef564d4f194a02cd559cc3f9199bddccb5 100644
--- a/core/genesis.go
+++ b/core/genesis.go
@@ -291,6 +291,9 @@ func (g *Genesis) ToBlock(db ethdb.Database) *types.Block {
 	if g.Difficulty == nil {
 		head.Difficulty = params.GenesisDifficulty
 	}
+	if g.Config != nil && g.Config.IsLondon(common.Big0) {
+		head.BaseFee = new(big.Int).SetUint64(params.InitialBaseFee)
+	}
 	statedb.Commit(false)
 	statedb.Database().TrieDB().Commit(root, true, nil)
 
diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go
index 05394321f74bb8593e960771a5968fc224e7ea04..ecdfa67f00dde7fe98bb0a71e068bd149cf69c83 100644
--- a/core/state_prefetcher.go
+++ b/core/state_prefetcher.go
@@ -63,7 +63,7 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c
 			return
 		}
 		// Convert the transaction into an executable message and pre-cache its sender
-		msg, err := tx.AsMessage(signer)
+		msg, err := tx.AsMessage(signer, header.BaseFee)
 		if err != nil {
 			return // Also invalid block, bail out
 		}
diff --git a/core/state_processor.go b/core/state_processor.go
index 40a953f0d4f87d4d390225c2f1e3385f74a64616..6f6bc1879b8d08e40c91d791a8a4b1b8f42e4eab 100644
--- a/core/state_processor.go
+++ b/core/state_processor.go
@@ -71,9 +71,9 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
 	vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg)
 	// Iterate over and process the individual transactions
 	for i, tx := range block.Transactions() {
-		msg, err := tx.AsMessage(types.MakeSigner(p.config, header.Number))
+		msg, err := tx.AsMessage(types.MakeSigner(p.config, header.Number), header.BaseFee)
 		if err != nil {
-			return nil, nil, 0, err
+			return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
 		}
 		statedb.Prepare(tx.Hash(), block.Hash(), i)
 		receipt, err := applyTransaction(msg, p.config, p.bc, nil, gp, statedb, header, tx, usedGas, vmenv)
@@ -139,7 +139,7 @@ func applyTransaction(msg types.Message, config *params.ChainConfig, bc ChainCon
 // for the transaction, gas used and an error if the transaction failed,
 // indicating the block was invalid.
 func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config) (*types.Receipt, error) {
-	msg, err := tx.AsMessage(types.MakeSigner(config, header.Number))
+	msg, err := tx.AsMessage(types.MakeSigner(config, header.Number), header.BaseFee)
 	if err != nil {
 		return nil, err
 	}
diff --git a/core/state_processor_test.go b/core/state_processor_test.go
index 5976ecc3d4e4f274ee3e6368f1fe75d2a5b1cf2f..28baf6e7d2705b10f9058109395dd3599a87732c 100644
--- a/core/state_processor_test.go
+++ b/core/state_processor_test.go
@@ -23,6 +23,7 @@ import (
 	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/consensus"
 	"github.com/ethereum/go-ethereum/consensus/ethash"
+	"github.com/ethereum/go-ethereum/consensus/misc"
 	"github.com/ethereum/go-ethereum/core/rawdb"
 	"github.com/ethereum/go-ethereum/core/types"
 	"github.com/ethereum/go-ethereum/core/vm"
@@ -38,75 +39,157 @@ import (
 // contain invalid transactions
 func TestStateProcessorErrors(t *testing.T) {
 	var (
-		signer     = types.HomesteadSigner{}
+		signer     = types.LatestSigner(params.TestChainConfig)
 		testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
-		db         = rawdb.NewMemoryDatabase()
-		gspec      = &Genesis{
-			Config: params.TestChainConfig,
-		}
-		genesis       = gspec.MustCommit(db)
-		blockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil)
 	)
-	defer blockchain.Stop()
 	var makeTx = func(nonce uint64, to common.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, data []byte) *types.Transaction {
 		tx, _ := types.SignTx(types.NewTransaction(nonce, to, amount, gasLimit, gasPrice, data), signer, testKey)
 		return tx
 	}
-	for i, tt := range []struct {
-		txs  []*types.Transaction
-		want string
-	}{
-		{
-			txs: []*types.Transaction{
-				makeTx(0, common.Address{}, big.NewInt(0), params.TxGas, nil, nil),
-				makeTx(0, common.Address{}, big.NewInt(0), params.TxGas, nil, nil),
+	var mkDynamicTx = func(nonce uint64, to common.Address, gasLimit uint64, tip, feeCap *big.Int) *types.Transaction {
+		tx, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{
+			Nonce:  nonce,
+			Tip:    tip,
+			FeeCap: feeCap,
+			Gas:    0,
+			To:     &to,
+			Value:  big.NewInt(0),
+		}), signer, testKey)
+		return tx
+	}
+	{ // Tests against a 'recent' chain definition
+		var (
+			db    = rawdb.NewMemoryDatabase()
+			gspec = &Genesis{
+				Config: params.TestChainConfig,
+				Alloc: GenesisAlloc{
+					common.HexToAddress("0x71562b71999873DB5b286dF957af199Ec94617F7"): GenesisAccount{
+						Balance: big.NewInt(1000000000000000000), // 1 ether
+						Nonce:   0,
+					},
+				},
+			}
+			genesis       = gspec.MustCommit(db)
+			blockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil)
+		)
+		defer blockchain.Stop()
+
+		for i, tt := range []struct {
+			txs  []*types.Transaction
+			want string
+		}{
+			{ // ErrNonceTooLow
+				txs: []*types.Transaction{
+					makeTx(0, common.Address{}, big.NewInt(0), params.TxGas, big.NewInt(875000000), nil),
+					makeTx(0, common.Address{}, big.NewInt(0), params.TxGas, big.NewInt(875000000), nil),
+				},
+				want: "could not apply tx 1 [0x0026256b3939ed97e2c4a6f3fce8ecf83bdcfa6d507c47838c308a1fb0436f62]: nonce too low: address 0x71562b71999873DB5b286dF957af199Ec94617F7, tx: 0 state: 1",
+			},
+			{ // ErrNonceTooHigh
+				txs: []*types.Transaction{
+					makeTx(100, common.Address{}, big.NewInt(0), params.TxGas, big.NewInt(875000000), nil),
+				},
+				want: "could not apply tx 0 [0xdebad714ca7f363bd0d8121c4518ad48fa469ca81b0a081be3d10c17460f751b]: nonce too high: address 0x71562b71999873DB5b286dF957af199Ec94617F7, tx: 100 state: 0",
+			},
+			{ // ErrGasLimitReached
+				txs: []*types.Transaction{
+					makeTx(0, common.Address{}, big.NewInt(0), 21000000, big.NewInt(875000000), nil),
+				},
+				want: "could not apply tx 0 [0xbd49d8dadfd47fb846986695f7d4da3f7b2c48c8da82dbc211a26eb124883de9]: gas limit reached",
 			},
-			want: "could not apply tx 1 [0x36bfa6d14f1cd35a1be8cc2322982a595fabc0e799f09c1de3bad7bd5b1f7626]: nonce too low: address 0x71562b71999873DB5b286dF957af199Ec94617F7, tx: 0 state: 1",
-		},
-		{
-			txs: []*types.Transaction{
-				makeTx(100, common.Address{}, big.NewInt(0), params.TxGas, nil, nil),
+			{ // ErrInsufficientFundsForTransfer
+				txs: []*types.Transaction{
+					makeTx(0, common.Address{}, big.NewInt(1000000000000000000), params.TxGas, big.NewInt(875000000), nil),
+				},
+				want: "could not apply tx 0 [0x98c796b470f7fcab40aaef5c965a602b0238e1034cce6fb73823042dd0638d74]: insufficient funds for transfer: address 0x71562b71999873DB5b286dF957af199Ec94617F7",
 			},
-			want: "could not apply tx 0 [0x51cd272d41ef6011d8138e18bf4043797aca9b713c7d39a97563f9bbe6bdbe6f]: nonce too high: address 0x71562b71999873DB5b286dF957af199Ec94617F7, tx: 100 state: 0",
-		},
-		{
-			txs: []*types.Transaction{
-				makeTx(0, common.Address{}, big.NewInt(0), 21000000, nil, nil),
+			{ // ErrInsufficientFunds
+				txs: []*types.Transaction{
+					makeTx(0, common.Address{}, big.NewInt(0), params.TxGas, big.NewInt(900000000000000000), nil),
+				},
+				want: "could not apply tx 0 [0x4a69690c4b0cd85e64d0d9ea06302455b01e10a83db964d60281739752003440]: insufficient funds for gas * price + value: address 0x71562b71999873DB5b286dF957af199Ec94617F7 have 1000000000000000000 want 18900000000000000000000",
 			},
-			want: "could not apply tx 0 [0x54c58b530824b0bb84b7a98183f08913b5d74e1cebc368515ef3c65edf8eb56a]: gas limit reached",
-		},
-		{
-			txs: []*types.Transaction{
-				makeTx(0, common.Address{}, big.NewInt(1), params.TxGas, nil, nil),
+			// ErrGasUintOverflow
+			// One missing 'core' error is ErrGasUintOverflow: "gas uint64 overflow",
+			// In order to trigger that one, we'd have to allocate a _huge_ chunk of data, such that the
+			// multiplication len(data) +gas_per_byte overflows uint64. Not testable at the moment
+			{ // ErrIntrinsicGas
+				txs: []*types.Transaction{
+					makeTx(0, common.Address{}, big.NewInt(0), params.TxGas-1000, big.NewInt(875000000), nil),
+				},
+				want: "could not apply tx 0 [0xcf3b049a0b516cb4f9274b3e2a264359e2ba53b2fb64b7bda2c634d5c9d01fca]: intrinsic gas too low: have 20000, want 21000",
 			},
-			want: "could not apply tx 0 [0x3094b17498940d92b13baccf356ce8bfd6f221e926abc903d642fa1466c5b50e]: insufficient funds for transfer: address 0x71562b71999873DB5b286dF957af199Ec94617F7",
-		},
-		{
-			txs: []*types.Transaction{
-				makeTx(0, common.Address{}, big.NewInt(0), params.TxGas, big.NewInt(0xffffff), nil),
+			{ // ErrGasLimitReached
+				txs: []*types.Transaction{
+					makeTx(0, common.Address{}, big.NewInt(0), params.TxGas*1000, big.NewInt(875000000), nil),
+				},
+				want: "could not apply tx 0 [0xbd49d8dadfd47fb846986695f7d4da3f7b2c48c8da82dbc211a26eb124883de9]: gas limit reached",
 			},
-			want: "could not apply tx 0 [0xaa3f7d86802b1f364576d9071bf231e31d61b392d306831ac9cf706ff5371ce0]: insufficient funds for gas * price + value: address 0x71562b71999873DB5b286dF957af199Ec94617F7 have 0 want 352321515000",
-		},
-		{
-			txs: []*types.Transaction{
-				makeTx(0, common.Address{}, big.NewInt(0), params.TxGas, nil, nil),
-				makeTx(1, common.Address{}, big.NewInt(0), params.TxGas, nil, nil),
-				makeTx(2, common.Address{}, big.NewInt(0), params.TxGas, nil, nil),
-				makeTx(3, common.Address{}, big.NewInt(0), params.TxGas-1000, big.NewInt(0), nil),
+			{ // ErrFeeCapTooLow
+				txs: []*types.Transaction{
+					mkDynamicTx(0, common.Address{}, params.TxGas-1000, big.NewInt(0), big.NewInt(0)),
+				},
+				want: "could not apply tx 0 [0x21e9b9015150fc7f6bd5059890a5e1727f2452df285e8a84f4ca61a74c159ded]: fee cap less than block base fee: address 0x71562b71999873DB5b286dF957af199Ec94617F7, feeCap: 0 baseFee: 875000000",
 			},
-			want: "could not apply tx 3 [0x836fab5882205362680e49b311a20646de03b630920f18ec6ee3b111a2cf6835]: intrinsic gas too low: have 20000, want 21000",
-		},
-		// The last 'core' error is ErrGasUintOverflow: "gas uint64 overflow", but in order to
-		// trigger that one, we'd have to allocate a _huge_ chunk of data, such that the
-		// multiplication len(data) +gas_per_byte overflows uint64. Not testable at the moment
-	} {
-		block := GenerateBadBlock(genesis, ethash.NewFaker(), tt.txs)
-		_, err := blockchain.InsertChain(types.Blocks{block})
-		if err == nil {
-			t.Fatal("block imported without errors")
+		} {
+			block := GenerateBadBlock(genesis, ethash.NewFaker(), tt.txs, gspec.Config)
+			_, err := blockchain.InsertChain(types.Blocks{block})
+			if err == nil {
+				t.Fatal("block imported without errors")
+			}
+			if have, want := err.Error(), tt.want; have != want {
+				t.Errorf("test %d:\nhave \"%v\"\nwant \"%v\"\n", i, have, want)
+			}
 		}
-		if have, want := err.Error(), tt.want; have != want {
-			t.Errorf("test %d:\nhave \"%v\"\nwant \"%v\"\n", i, have, want)
+	}
+
+	// One final error is ErrTxTypeNotSupported. For this, we need an older chain
+	{
+		var (
+			db    = rawdb.NewMemoryDatabase()
+			gspec = &Genesis{
+				Config: &params.ChainConfig{
+					ChainID:             big.NewInt(1),
+					HomesteadBlock:      big.NewInt(0),
+					EIP150Block:         big.NewInt(0),
+					EIP155Block:         big.NewInt(0),
+					EIP158Block:         big.NewInt(0),
+					ByzantiumBlock:      big.NewInt(0),
+					ConstantinopleBlock: big.NewInt(0),
+					PetersburgBlock:     big.NewInt(0),
+					IstanbulBlock:       big.NewInt(0),
+					MuirGlacierBlock:    big.NewInt(0),
+				},
+				Alloc: GenesisAlloc{
+					common.HexToAddress("0x71562b71999873DB5b286dF957af199Ec94617F7"): GenesisAccount{
+						Balance: big.NewInt(1000000000000000000), // 1 ether
+						Nonce:   0,
+					},
+				},
+			}
+			genesis       = gspec.MustCommit(db)
+			blockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil)
+		)
+		defer blockchain.Stop()
+		for i, tt := range []struct {
+			txs  []*types.Transaction
+			want string
+		}{
+			{ // ErrTxTypeNotSupported
+				txs: []*types.Transaction{
+					mkDynamicTx(0, common.Address{}, params.TxGas-1000, big.NewInt(0), big.NewInt(0)),
+				},
+				want: "could not apply tx 0 [0x21e9b9015150fc7f6bd5059890a5e1727f2452df285e8a84f4ca61a74c159ded]: transaction type not supported",
+			},
+		} {
+			block := GenerateBadBlock(genesis, ethash.NewFaker(), tt.txs, gspec.Config)
+			_, err := blockchain.InsertChain(types.Blocks{block})
+			if err == nil {
+				t.Fatal("block imported without errors")
+			}
+			if have, want := err.Error(), tt.want; have != want {
+				t.Errorf("test %d:\nhave \"%v\"\nwant \"%v\"\n", i, have, want)
+			}
 		}
 	}
 }
@@ -115,11 +198,11 @@ func TestStateProcessorErrors(t *testing.T) {
 // valid, and no proper post-state can be made. But from the perspective of the blockchain, the block is sufficiently
 // valid to be considered for import:
 // - valid pow (fake), ancestry, difficulty, gaslimit etc
-func GenerateBadBlock(parent *types.Block, engine consensus.Engine, txs types.Transactions) *types.Block {
+func GenerateBadBlock(parent *types.Block, engine consensus.Engine, txs types.Transactions, config *params.ChainConfig) *types.Block {
 	header := &types.Header{
 		ParentHash: parent.Hash(),
 		Coinbase:   parent.Coinbase(),
-		Difficulty: engine.CalcDifficulty(&fakeChainReader{params.TestChainConfig}, parent.Time()+10, &types.Header{
+		Difficulty: engine.CalcDifficulty(&fakeChainReader{config}, parent.Time()+10, &types.Header{
 			Number:     parent.Number(),
 			Time:       parent.Time(),
 			Difficulty: parent.Difficulty(),
@@ -130,8 +213,10 @@ func GenerateBadBlock(parent *types.Block, engine consensus.Engine, txs types.Tr
 		Time:      parent.Time() + 10,
 		UncleHash: types.EmptyUncleHash,
 	}
+	if config.IsLondon(header.Number) {
+		header.BaseFee = misc.CalcBaseFee(config, parent.Header())
+	}
 	var receipts []*types.Receipt
-
 	// The post-state result doesn't need to be correct (this is a bad block), but we do need something there
 	// Preferably something unique. So let's use a combo of blocknum + txhash
 	hasher := sha3.NewLegacyKeccak256()
diff --git a/core/state_transition.go b/core/state_transition.go
index 05becd9a00580456e06713d31cb90f79cc1a36e1..881d34f4bcd8e37691d4865288b9d2f20ff454f7 100644
--- a/core/state_transition.go
+++ b/core/state_transition.go
@@ -22,6 +22,7 @@ import (
 	"math/big"
 
 	"github.com/ethereum/go-ethereum/common"
+	cmath "github.com/ethereum/go-ethereum/common/math"
 	"github.com/ethereum/go-ethereum/core/types"
 	"github.com/ethereum/go-ethereum/core/vm"
 	"github.com/ethereum/go-ethereum/params"
@@ -49,6 +50,8 @@ type StateTransition struct {
 	msg        Message
 	gas        uint64
 	gasPrice   *big.Int
+	feeCap     *big.Int
+	tip        *big.Int
 	initialGas uint64
 	value      *big.Int
 	data       []byte
@@ -62,6 +65,8 @@ type Message interface {
 	To() *common.Address
 
 	GasPrice() *big.Int
+	FeeCap() *big.Int
+	Tip() *big.Int
 	Gas() uint64
 	Value() *big.Int
 
@@ -154,6 +159,8 @@ func NewStateTransition(evm *vm.EVM, msg Message, gp *GasPool) *StateTransition
 		evm:      evm,
 		msg:      msg,
 		gasPrice: msg.GasPrice(),
+		feeCap:   msg.FeeCap(),
+		tip:      msg.Tip(),
 		value:    msg.Value(),
 		data:     msg.Data(),
 		state:    evm.StateDB,
@@ -206,6 +213,15 @@ func (st *StateTransition) preCheck() error {
 				st.msg.From().Hex(), msgNonce, stNonce)
 		}
 	}
+	// Make sure that transaction feeCap is greater than the baseFee (post london)
+	if st.evm.ChainConfig().IsLondon(st.evm.Context.BlockNumber) {
+		// This will panic if baseFee is nil, but basefee presence is verified
+		// as part of header validation.
+		if st.feeCap.Cmp(st.evm.Context.BaseFee) < 0 {
+			return fmt.Errorf("%w: address %v, feeCap: %s baseFee: %s", ErrFeeCapTooLow,
+				st.msg.From().Hex(), st.feeCap, st.evm.Context.BaseFee)
+		}
+	}
 	return st.buyGas()
 }
 
@@ -281,7 +297,11 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
 		// After EIP-3529: refunds are capped to gasUsed / 5
 		st.refundGas(params.RefundQuotientEIP3529)
 	}
-	st.state.AddBalance(st.evm.Context.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice))
+	effectiveTip := st.gasPrice
+	if st.evm.ChainConfig().IsLondon(st.evm.Context.BlockNumber) {
+		effectiveTip = cmath.BigMin(st.tip, new(big.Int).Sub(st.feeCap, st.evm.Context.BaseFee))
+	}
+	st.state.AddBalance(st.evm.Context.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), effectiveTip))
 
 	return &ExecutionResult{
 		UsedGas:    st.gasUsed(),
diff --git a/core/types/access_list_tx.go b/core/types/access_list_tx.go
index 65ee95adf611d1e82982b989b86b18aaf3e8b207..48102a1d407564ae6cb0a26181faab0583149e84 100644
--- a/core/types/access_list_tx.go
+++ b/core/types/access_list_tx.go
@@ -94,7 +94,6 @@ func (tx *AccessListTx) copy() TxData {
 }
 
 // accessors for innerTx.
-
 func (tx *AccessListTx) txType() byte           { return AccessListTxType }
 func (tx *AccessListTx) chainID() *big.Int      { return tx.ChainID }
 func (tx *AccessListTx) protected() bool        { return true }
@@ -102,6 +101,8 @@ func (tx *AccessListTx) accessList() AccessList { return tx.AccessList }
 func (tx *AccessListTx) data() []byte           { return tx.Data }
 func (tx *AccessListTx) gas() uint64            { return tx.Gas }
 func (tx *AccessListTx) gasPrice() *big.Int     { return tx.GasPrice }
+func (tx *AccessListTx) tip() *big.Int          { return tx.GasPrice }
+func (tx *AccessListTx) feeCap() *big.Int       { return tx.GasPrice }
 func (tx *AccessListTx) value() *big.Int        { return tx.Value }
 func (tx *AccessListTx) nonce() uint64          { return tx.Nonce }
 func (tx *AccessListTx) to() *common.Address    { return tx.To }
diff --git a/core/types/block.go b/core/types/block.go
index a3318f8779d6cc85d0be14985623188a93d2f910..5f3dbb957b6d3628b62df2be8b835eff578a0739 100644
--- a/core/types/block.go
+++ b/core/types/block.go
@@ -82,6 +82,9 @@ type Header struct {
 	Extra       []byte         `json:"extraData"        gencodec:"required"`
 	MixDigest   common.Hash    `json:"mixHash"`
 	Nonce       BlockNonce     `json:"nonce"`
+
+	// BaseFee was added by EIP-1559 and is ignored in legacy headers.
+	BaseFee *big.Int `json:"baseFee" rlp:"optional"`
 }
 
 // field type overrides for gencodec
@@ -92,6 +95,7 @@ type headerMarshaling struct {
 	GasUsed    hexutil.Uint64
 	Time       hexutil.Uint64
 	Extra      hexutil.Bytes
+	BaseFee    *hexutil.Big
 	Hash       common.Hash `json:"hash"` // adds call to Hash() in MarshalJSON
 }
 
@@ -229,6 +233,9 @@ func CopyHeader(h *Header) *Header {
 	if cpy.Number = new(big.Int); h.Number != nil {
 		cpy.Number.Set(h.Number)
 	}
+	if h.BaseFee != nil {
+		cpy.BaseFee = new(big.Int).Set(h.BaseFee)
+	}
 	if len(h.Extra) > 0 {
 		cpy.Extra = make([]byte, len(h.Extra))
 		copy(cpy.Extra, h.Extra)
@@ -289,6 +296,13 @@ func (b *Block) ReceiptHash() common.Hash { return b.header.ReceiptHash }
 func (b *Block) UncleHash() common.Hash   { return b.header.UncleHash }
 func (b *Block) Extra() []byte            { return common.CopyBytes(b.header.Extra) }
 
+func (b *Block) BaseFee() *big.Int {
+	if b.header.BaseFee == nil {
+		return nil
+	}
+	return new(big.Int).Set(b.header.BaseFee)
+}
+
 func (b *Block) Header() *Header { return CopyHeader(b.header) }
 
 // Body returns the non-header content of the block.
diff --git a/core/types/block_test.go b/core/types/block_test.go
index 63904f882c924652f447dfccaaf21c097bfe5ff3..9ecb1a4d8f1978157a54aa3021491adb4336026b 100644
--- a/core/types/block_test.go
+++ b/core/types/block_test.go
@@ -68,6 +68,71 @@ func TestBlockEncoding(t *testing.T) {
 	}
 }
 
+func TestEIP1559BlockEncoding(t *testing.T) {
+	blockEnc := common.FromHex("f9030bf901fea083cafc574e1f51ba9dc0568fc617a08ea2429fb384059c972f13b19fa1c8dd55a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347948888f1f195afa192cfee860698584c030f4c9db1a0ef1552a40b7165c3cd773806b9e0c165b75356e0314bf0706f279c729f51e017a05fe50b260da6308036625b850b5d6ced6d0a9f814c0688bc91ffb7b7a3a54b67a0bc37d79753ad738a6dac4921e57392f145d8887476de3f783dfa7edae9283e52b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008302000001832fefd8825208845506eb0780a0bd4472abb6659ebe3ee06ee4d7b72a00a9f4d001caca51342001075469aff49888a13a5a8c8f2bb1c4843b9aca00f90106f85f800a82c35094095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba09bea4c4daac7c7c52e093e6a4c35dbbcf8856f1af7b059ba20253e70848d094fa08a8fae537ce25ed8cb5af9adac3f141af69bd515bd2ba031522df09b97dd72b1b8a302f8a0018080843b9aca008301e24194095e7baea6a6c7c4c2dfeb977efac326af552d878080f838f7940000000000000000000000000000000000000001e1a0000000000000000000000000000000000000000000000000000000000000000080a0fe38ca4e44a30002ac54af7cf922a6ac2ba11b7d22f548e8ecb3f51f41cb31b0a06de6a5cbae13c0c856e33acf021b51819636cfc009d39eafb9f606d546e305a8c0")
+	var block Block
+	if err := rlp.DecodeBytes(blockEnc, &block); err != nil {
+		t.Fatal("decode error: ", err)
+	}
+
+	check := func(f string, got, want interface{}) {
+		if !reflect.DeepEqual(got, want) {
+			t.Errorf("%s mismatch: got %v, want %v", f, got, want)
+		}
+	}
+
+	check("Difficulty", block.Difficulty(), big.NewInt(131072))
+	check("GasLimit", block.GasLimit(), uint64(3141592))
+	check("GasUsed", block.GasUsed(), uint64(21000))
+	check("Coinbase", block.Coinbase(), common.HexToAddress("8888f1f195afa192cfee860698584c030f4c9db1"))
+	check("MixDigest", block.MixDigest(), common.HexToHash("bd4472abb6659ebe3ee06ee4d7b72a00a9f4d001caca51342001075469aff498"))
+	check("Root", block.Root(), common.HexToHash("ef1552a40b7165c3cd773806b9e0c165b75356e0314bf0706f279c729f51e017"))
+	check("Hash", block.Hash(), common.HexToHash("c7252048cd273fe0dac09650027d07f0e3da4ee0675ebbb26627cea92729c372"))
+	check("Nonce", block.Nonce(), uint64(0xa13a5a8c8f2bb1c4))
+	check("Time", block.Time(), uint64(1426516743))
+	check("Size", block.Size(), common.StorageSize(len(blockEnc)))
+	check("BaseFee", block.BaseFee(), new(big.Int).SetUint64(params.InitialBaseFee))
+
+	tx1 := NewTransaction(0, common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), big.NewInt(10), 50000, big.NewInt(10), nil)
+	tx1, _ = tx1.WithSignature(HomesteadSigner{}, common.Hex2Bytes("9bea4c4daac7c7c52e093e6a4c35dbbcf8856f1af7b059ba20253e70848d094f8a8fae537ce25ed8cb5af9adac3f141af69bd515bd2ba031522df09b97dd72b100"))
+
+	addr := common.HexToAddress("0x0000000000000000000000000000000000000001")
+	accesses := AccessList{AccessTuple{
+		Address: addr,
+		StorageKeys: []common.Hash{
+			{0},
+		},
+	}}
+	to := common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87")
+	txdata := &DynamicFeeTx{
+		ChainID:    big.NewInt(1),
+		Nonce:      0,
+		To:         &to,
+		Gas:        123457,
+		FeeCap:     new(big.Int).Set(block.BaseFee()),
+		Tip:        big.NewInt(0),
+		AccessList: accesses,
+		Data:       []byte{},
+	}
+	tx2 := NewTx(txdata)
+	tx2, err := tx2.WithSignature(LatestSignerForChainID(big.NewInt(1)), common.Hex2Bytes("fe38ca4e44a30002ac54af7cf922a6ac2ba11b7d22f548e8ecb3f51f41cb31b06de6a5cbae13c0c856e33acf021b51819636cfc009d39eafb9f606d546e305a800"))
+	if err != nil {
+		t.Fatal("invalid signature error: ", err)
+	}
+
+	check("len(Transactions)", len(block.Transactions()), 2)
+	check("Transactions[0].Hash", block.Transactions()[0].Hash(), tx1.Hash())
+	check("Transactions[1].Hash", block.Transactions()[1].Hash(), tx2.Hash())
+	check("Transactions[1].Type", block.Transactions()[1].Type(), tx2.Type())
+	ourBlockEnc, err := rlp.EncodeToBytes(&block)
+	if err != nil {
+		t.Fatal("encode error: ", err)
+	}
+	if !bytes.Equal(ourBlockEnc, blockEnc) {
+		t.Errorf("encoded block mismatch:\ngot:  %x\nwant: %x", ourBlockEnc, blockEnc)
+	}
+}
+
 func TestEIP2718BlockEncoding(t *testing.T) {
 	blockEnc := common.FromHex("f90319f90211a00000000000000000000000000000000000000000000000000000000000000000a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347948888f1f195afa192cfee860698584c030f4c9db1a0ef1552a40b7165c3cd773806b9e0c165b75356e0314bf0706f279c729f51e017a0e6e49996c7ec59f7a23d22b83239a60151512c65613bf84a0d7da336399ebc4aa0cafe75574d59780665a97fbfd11365c7545aa8f1abf4e5e12e8243334ef7286bb901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000820200832fefd882a410845506eb0796636f6f6c65737420626c6f636b206f6e20636861696ea0bd4472abb6659ebe3ee06ee4d7b72a00a9f4d001caca51342001075469aff49888a13a5a8c8f2bb1c4f90101f85f800a82c35094095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba09bea4c4daac7c7c52e093e6a4c35dbbcf8856f1af7b059ba20253e70848d094fa08a8fae537ce25ed8cb5af9adac3f141af69bd515bd2ba031522df09b97dd72b1b89e01f89b01800a8301e24194095e7baea6a6c7c4c2dfeb977efac326af552d878080f838f7940000000000000000000000000000000000000001e1a0000000000000000000000000000000000000000000000000000000000000000001a03dbacc8d0259f2508625e97fdfc57cd85fdd16e5821bc2c10bdd1a52649e8335a0476e10695b183a87b0aa292a7f4b78ef0c3fbe62aa2c42c84e1d9c3da159ef14c0")
 	var block Block
diff --git a/core/types/dynamic_fee_tx.go b/core/types/dynamic_fee_tx.go
new file mode 100644
index 0000000000000000000000000000000000000000..777babe02712fe3d76c1e237a828df0e824b2c80
--- /dev/null
+++ b/core/types/dynamic_fee_tx.go
@@ -0,0 +1,104 @@
+// Copyright 2021 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library 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 Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package types
+
+import (
+	"math/big"
+
+	"github.com/ethereum/go-ethereum/common"
+)
+
+type DynamicFeeTx struct {
+	ChainID    *big.Int
+	Nonce      uint64
+	Tip        *big.Int
+	FeeCap     *big.Int
+	Gas        uint64
+	To         *common.Address `rlp:"nil"` // nil means contract creation
+	Value      *big.Int
+	Data       []byte
+	AccessList AccessList
+
+	// Signature values
+	V *big.Int `json:"v" gencodec:"required"`
+	R *big.Int `json:"r" gencodec:"required"`
+	S *big.Int `json:"s" gencodec:"required"`
+}
+
+// copy creates a deep copy of the transaction data and initializes all fields.
+func (tx *DynamicFeeTx) copy() TxData {
+	cpy := &DynamicFeeTx{
+		Nonce: tx.Nonce,
+		To:    tx.To, // TODO: copy pointed-to address
+		Data:  common.CopyBytes(tx.Data),
+		Gas:   tx.Gas,
+		// These are copied below.
+		AccessList: make(AccessList, len(tx.AccessList)),
+		Value:      new(big.Int),
+		ChainID:    new(big.Int),
+		Tip:        new(big.Int),
+		FeeCap:     new(big.Int),
+		V:          new(big.Int),
+		R:          new(big.Int),
+		S:          new(big.Int),
+	}
+	copy(cpy.AccessList, tx.AccessList)
+	if tx.Value != nil {
+		cpy.Value.Set(tx.Value)
+	}
+	if tx.ChainID != nil {
+		cpy.ChainID.Set(tx.ChainID)
+	}
+	if tx.Tip != nil {
+		cpy.Tip.Set(tx.Tip)
+	}
+	if tx.FeeCap != nil {
+		cpy.FeeCap.Set(tx.FeeCap)
+	}
+	if tx.V != nil {
+		cpy.V.Set(tx.V)
+	}
+	if tx.R != nil {
+		cpy.R.Set(tx.R)
+	}
+	if tx.S != nil {
+		cpy.S.Set(tx.S)
+	}
+	return cpy
+}
+
+// accessors for innerTx.
+func (tx *DynamicFeeTx) txType() byte           { return DynamicFeeTxType }
+func (tx *DynamicFeeTx) chainID() *big.Int      { return tx.ChainID }
+func (tx *DynamicFeeTx) protected() bool        { return true }
+func (tx *DynamicFeeTx) accessList() AccessList { return tx.AccessList }
+func (tx *DynamicFeeTx) data() []byte           { return tx.Data }
+func (tx *DynamicFeeTx) gas() uint64            { return tx.Gas }
+func (tx *DynamicFeeTx) feeCap() *big.Int       { return tx.FeeCap }
+func (tx *DynamicFeeTx) tip() *big.Int          { return tx.Tip }
+func (tx *DynamicFeeTx) gasPrice() *big.Int     { return tx.FeeCap }
+func (tx *DynamicFeeTx) value() *big.Int        { return tx.Value }
+func (tx *DynamicFeeTx) nonce() uint64          { return tx.Nonce }
+func (tx *DynamicFeeTx) to() *common.Address    { return tx.To }
+
+func (tx *DynamicFeeTx) rawSignatureValues() (v, r, s *big.Int) {
+	return tx.V, tx.R, tx.S
+}
+
+func (tx *DynamicFeeTx) setSignatureValues(chainID, v, r, s *big.Int) {
+	tx.ChainID, tx.V, tx.R, tx.S = chainID, v, r, s
+}
diff --git a/core/types/legacy_tx.go b/core/types/legacy_tx.go
index 41ad44f37985a0935905396e54739cda3a3a8e54..f5e0f70783ae266e41c38ae5950ce681d2e88b38 100644
--- a/core/types/legacy_tx.go
+++ b/core/types/legacy_tx.go
@@ -91,13 +91,14 @@ func (tx *LegacyTx) copy() TxData {
 }
 
 // accessors for innerTx.
-
 func (tx *LegacyTx) txType() byte           { return LegacyTxType }
 func (tx *LegacyTx) chainID() *big.Int      { return deriveChainId(tx.V) }
 func (tx *LegacyTx) accessList() AccessList { return nil }
 func (tx *LegacyTx) data() []byte           { return tx.Data }
 func (tx *LegacyTx) gas() uint64            { return tx.Gas }
 func (tx *LegacyTx) gasPrice() *big.Int     { return tx.GasPrice }
+func (tx *LegacyTx) tip() *big.Int          { return tx.GasPrice }
+func (tx *LegacyTx) feeCap() *big.Int       { return tx.GasPrice }
 func (tx *LegacyTx) value() *big.Int        { return tx.Value }
 func (tx *LegacyTx) nonce() uint64          { return tx.Nonce }
 func (tx *LegacyTx) to() *common.Address    { return tx.To }
diff --git a/core/types/receipt.go b/core/types/receipt.go
index 6b519a79d221c54a179fa692e29b24a9f0009756..b949bd2bd5f345a4f7f039a91e19c0e4722eb74a 100644
--- a/core/types/receipt.go
+++ b/core/types/receipt.go
@@ -120,10 +120,6 @@ func (r *Receipt) EncodeRLP(w io.Writer) error {
 	if r.Type == LegacyTxType {
 		return rlp.Encode(w, data)
 	}
-	// It's an EIP-2718 typed TX receipt.
-	if r.Type != AccessListTxType {
-		return ErrTxTypeNotSupported
-	}
 	buf := encodeBufferPool.Get().(*bytes.Buffer)
 	defer encodeBufferPool.Put(buf)
 	buf.Reset()
@@ -159,7 +155,7 @@ func (r *Receipt) DecodeRLP(s *rlp.Stream) error {
 			return errEmptyTypedReceipt
 		}
 		r.Type = b[0]
-		if r.Type == AccessListTxType {
+		if r.Type == AccessListTxType || r.Type == DynamicFeeTxType {
 			var dec receiptRLP
 			if err := rlp.DecodeBytes(b[1:], &dec); err != nil {
 				return err
@@ -263,6 +259,9 @@ func (rs Receipts) EncodeIndex(i int, w *bytes.Buffer) {
 	case AccessListTxType:
 		w.WriteByte(AccessListTxType)
 		rlp.Encode(w, data)
+	case DynamicFeeTxType:
+		w.WriteByte(DynamicFeeTxType)
+		rlp.Encode(w, data)
 	default:
 		// For unsupported types, write nothing. Since this is for
 		// DeriveSha, the error will be caught matching the derived hash
diff --git a/core/types/transaction.go b/core/types/transaction.go
index a35e07a5a3161b3c94ee9b814da8a8ed8cce9944..ace1843e932b644f8968cf1e5d59e4c1161cb1df 100644
--- a/core/types/transaction.go
+++ b/core/types/transaction.go
@@ -26,6 +26,7 @@ import (
 	"time"
 
 	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/common/math"
 	"github.com/ethereum/go-ethereum/crypto"
 	"github.com/ethereum/go-ethereum/rlp"
 )
@@ -42,6 +43,7 @@ var (
 const (
 	LegacyTxType = iota
 	AccessListTxType
+	DynamicFeeTxType
 )
 
 // Transaction is an Ethereum transaction.
@@ -74,6 +76,8 @@ type TxData interface {
 	data() []byte
 	gas() uint64
 	gasPrice() *big.Int
+	tip() *big.Int
+	feeCap() *big.Int
 	value() *big.Int
 	nonce() uint64
 	to() *common.Address
@@ -177,6 +181,10 @@ func (tx *Transaction) decodeTyped(b []byte) (TxData, error) {
 		var inner AccessListTx
 		err := rlp.DecodeBytes(b[1:], &inner)
 		return &inner, err
+	case DynamicFeeTxType:
+		var inner DynamicFeeTx
+		err := rlp.DecodeBytes(b[1:], &inner)
+		return &inner, err
 	default:
 		return nil, ErrTxTypeNotSupported
 	}
@@ -260,6 +268,12 @@ func (tx *Transaction) Gas() uint64 { return tx.inner.gas() }
 // GasPrice returns the gas price of the transaction.
 func (tx *Transaction) GasPrice() *big.Int { return new(big.Int).Set(tx.inner.gasPrice()) }
 
+// Tip returns the tip per gas of the transaction.
+func (tx *Transaction) Tip() *big.Int { return new(big.Int).Set(tx.inner.tip()) }
+
+// FeeCap returns the fee cap per gas of the transaction.
+func (tx *Transaction) FeeCap() *big.Int { return new(big.Int).Set(tx.inner.feeCap()) }
+
 // Value returns the ether amount of the transaction.
 func (tx *Transaction) Value() *big.Int { return new(big.Int).Set(tx.inner.value()) }
 
@@ -486,12 +500,14 @@ type Message struct {
 	amount     *big.Int
 	gasLimit   uint64
 	gasPrice   *big.Int
+	feeCap     *big.Int
+	tip        *big.Int
 	data       []byte
 	accessList AccessList
 	checkNonce bool
 }
 
-func NewMessage(from common.Address, to *common.Address, nonce uint64, amount *big.Int, gasLimit uint64, gasPrice *big.Int, data []byte, accessList AccessList, checkNonce bool) Message {
+func NewMessage(from common.Address, to *common.Address, nonce uint64, amount *big.Int, gasLimit uint64, gasPrice, feeCap, tip *big.Int, data []byte, accessList AccessList, checkNonce bool) Message {
 	return Message{
 		from:       from,
 		to:         to,
@@ -499,6 +515,8 @@ func NewMessage(from common.Address, to *common.Address, nonce uint64, amount *b
 		amount:     amount,
 		gasLimit:   gasLimit,
 		gasPrice:   gasPrice,
+		feeCap:     feeCap,
+		tip:        tip,
 		data:       data,
 		accessList: accessList,
 		checkNonce: checkNonce,
@@ -506,11 +524,13 @@ func NewMessage(from common.Address, to *common.Address, nonce uint64, amount *b
 }
 
 // AsMessage returns the transaction as a core.Message.
-func (tx *Transaction) AsMessage(s Signer) (Message, error) {
+func (tx *Transaction) AsMessage(s Signer, baseFee *big.Int) (Message, error) {
 	msg := Message{
 		nonce:      tx.Nonce(),
 		gasLimit:   tx.Gas(),
 		gasPrice:   new(big.Int).Set(tx.GasPrice()),
+		feeCap:     new(big.Int).Set(tx.FeeCap()),
+		tip:        new(big.Int).Set(tx.Tip()),
 		to:         tx.To(),
 		amount:     tx.Value(),
 		data:       tx.Data(),
@@ -518,6 +538,11 @@ func (tx *Transaction) AsMessage(s Signer) (Message, error) {
 		checkNonce: true,
 	}
 
+	// If baseFee provided, set gasPrice to effectiveGasPrice.
+	if baseFee != nil {
+		msg.gasPrice = math.BigMin(msg.gasPrice.Add(msg.tip, baseFee), msg.feeCap)
+	}
+
 	var err error
 	msg.from, err = Sender(s, tx)
 	return msg, err
@@ -526,6 +551,8 @@ func (tx *Transaction) AsMessage(s Signer) (Message, error) {
 func (m Message) From() common.Address   { return m.from }
 func (m Message) To() *common.Address    { return m.to }
 func (m Message) GasPrice() *big.Int     { return m.gasPrice }
+func (m Message) FeeCap() *big.Int       { return m.feeCap }
+func (m Message) Tip() *big.Int          { return m.tip }
 func (m Message) Value() *big.Int        { return m.amount }
 func (m Message) Gas() uint64            { return m.gasLimit }
 func (m Message) Nonce() uint64          { return m.nonce }
diff --git a/core/types/transaction_marshalling.go b/core/types/transaction_marshalling.go
index e56148555654214447e303b4a09b8b5a0e62751e..ecdbd70afa1be82a3442cfac82820309d670c492 100644
--- a/core/types/transaction_marshalling.go
+++ b/core/types/transaction_marshalling.go
@@ -30,15 +30,19 @@ type txJSON struct {
 	Type hexutil.Uint64 `json:"type"`
 
 	// Common transaction fields:
-	Nonce    *hexutil.Uint64 `json:"nonce"`
-	GasPrice *hexutil.Big    `json:"gasPrice"`
-	Gas      *hexutil.Uint64 `json:"gas"`
-	Value    *hexutil.Big    `json:"value"`
-	Data     *hexutil.Bytes  `json:"input"`
-	V        *hexutil.Big    `json:"v"`
-	R        *hexutil.Big    `json:"r"`
-	S        *hexutil.Big    `json:"s"`
-	To       *common.Address `json:"to"`
+	Nonce                *hexutil.Uint64 `json:"nonce"`
+	GasPrice             *hexutil.Big    `json:"gasPrice"`
+	FeeCap               *hexutil.Big    `json:"feeCap"`
+	Tip                  *hexutil.Big    `json:"tip"`
+	MaxPriorityFeePerGas *hexutil.Big    `json:"maxPriorityFeePerGas"`
+	MaxFeePerGas         *hexutil.Big    `json:"maxFeePerGas"`
+	Gas                  *hexutil.Uint64 `json:"gas"`
+	Value                *hexutil.Big    `json:"value"`
+	Data                 *hexutil.Bytes  `json:"input"`
+	V                    *hexutil.Big    `json:"v"`
+	R                    *hexutil.Big    `json:"r"`
+	S                    *hexutil.Big    `json:"s"`
+	To                   *common.Address `json:"to"`
 
 	// Access list transaction fields:
 	ChainID    *hexutil.Big `json:"chainId,omitempty"`
@@ -79,6 +83,19 @@ func (t *Transaction) MarshalJSON() ([]byte, error) {
 		enc.V = (*hexutil.Big)(tx.V)
 		enc.R = (*hexutil.Big)(tx.R)
 		enc.S = (*hexutil.Big)(tx.S)
+	case *DynamicFeeTx:
+		enc.ChainID = (*hexutil.Big)(tx.ChainID)
+		enc.AccessList = &tx.AccessList
+		enc.Nonce = (*hexutil.Uint64)(&tx.Nonce)
+		enc.Gas = (*hexutil.Uint64)(&tx.Gas)
+		enc.FeeCap = (*hexutil.Big)(tx.FeeCap)
+		enc.Tip = (*hexutil.Big)(tx.Tip)
+		enc.Value = (*hexutil.Big)(tx.Value)
+		enc.Data = (*hexutil.Bytes)(&tx.Data)
+		enc.To = t.To()
+		enc.V = (*hexutil.Big)(tx.V)
+		enc.R = (*hexutil.Big)(tx.R)
+		enc.S = (*hexutil.Big)(tx.S)
 	}
 	return json.Marshal(&enc)
 }
@@ -191,6 +208,75 @@ func (t *Transaction) UnmarshalJSON(input []byte) error {
 			}
 		}
 
+	case DynamicFeeTxType:
+		var itx DynamicFeeTx
+		inner = &itx
+		// Access list is optional for now.
+		if dec.AccessList != nil {
+			itx.AccessList = *dec.AccessList
+		}
+		if dec.ChainID == nil {
+			return errors.New("missing required field 'chainId' in transaction")
+		}
+		itx.ChainID = (*big.Int)(dec.ChainID)
+		if dec.To != nil {
+			itx.To = dec.To
+		}
+		if dec.Nonce == nil {
+			return errors.New("missing required field 'nonce' in transaction")
+		}
+		itx.Nonce = uint64(*dec.Nonce)
+		switch {
+		case dec.Tip == nil && dec.MaxPriorityFeePerGas == nil:
+			return errors.New("at least one of 'tip' or 'maxPriorityFeePerGas' must be defined")
+		case dec.Tip != nil && dec.MaxPriorityFeePerGas != nil:
+			return errors.New("only one of 'tip' or 'maxPriorityFeePerGas' may be defined")
+		case dec.Tip != nil && dec.MaxPriorityFeePerGas == nil:
+			itx.Tip = (*big.Int)(dec.Tip)
+		case dec.Tip == nil && dec.MaxPriorityFeePerGas != nil:
+			itx.Tip = (*big.Int)(dec.MaxPriorityFeePerGas)
+		}
+		switch {
+		case dec.FeeCap == nil && dec.MaxFeePerGas == nil:
+			return errors.New("at least one of 'feeCap' or 'maxFeePerGas' must be defined")
+		case dec.FeeCap != nil && dec.MaxFeePerGas != nil:
+			return errors.New("only one of 'feeCap' or 'maxFeePerGas' may be defined")
+		case dec.FeeCap != nil && dec.MaxFeePerGas == nil:
+			itx.FeeCap = (*big.Int)(dec.FeeCap)
+		case dec.FeeCap == nil && dec.MaxFeePerGas != nil:
+			itx.FeeCap = (*big.Int)(dec.MaxFeePerGas)
+		}
+		if dec.Gas == nil {
+			return errors.New("missing required field 'gas' for txdata")
+		}
+		itx.Gas = uint64(*dec.Gas)
+		if dec.Value == nil {
+			return errors.New("missing required field 'value' in transaction")
+		}
+		itx.Value = (*big.Int)(dec.Value)
+		if dec.Data == nil {
+			return errors.New("missing required field 'input' in transaction")
+		}
+		itx.Data = *dec.Data
+		if dec.V == nil {
+			return errors.New("missing required field 'v' in transaction")
+		}
+		itx.V = (*big.Int)(dec.V)
+		if dec.R == nil {
+			return errors.New("missing required field 'r' in transaction")
+		}
+		itx.R = (*big.Int)(dec.R)
+		if dec.S == nil {
+			return errors.New("missing required field 's' in transaction")
+		}
+		itx.S = (*big.Int)(dec.S)
+		withSignature := itx.V.Sign() != 0 || itx.R.Sign() != 0 || itx.S.Sign() != 0
+		if withSignature {
+			if err := sanityCheckSignature(itx.V, itx.R, itx.S, false); err != nil {
+				return err
+			}
+		}
+
 	default:
 		return ErrTxTypeNotSupported
 	}
diff --git a/core/types/transaction_signing.go b/core/types/transaction_signing.go
index 5d94b26b36472321c96116d755b3f96ca739e677..e553e6af9665fb59ee0a7186e5b3c3cbafbb9aba 100644
--- a/core/types/transaction_signing.go
+++ b/core/types/transaction_signing.go
@@ -40,6 +40,8 @@ type sigCache struct {
 func MakeSigner(config *params.ChainConfig, blockNumber *big.Int) Signer {
 	var signer Signer
 	switch {
+	case config.IsLondon(blockNumber):
+		signer = NewLondonSigner(config.ChainID)
 	case config.IsBerlin(blockNumber):
 		signer = NewEIP2930Signer(config.ChainID)
 	case config.IsEIP155(blockNumber):
@@ -61,6 +63,9 @@ func MakeSigner(config *params.ChainConfig, blockNumber *big.Int) Signer {
 // have the current block number available, use MakeSigner instead.
 func LatestSigner(config *params.ChainConfig) Signer {
 	if config.ChainID != nil {
+		if config.LondonBlock != nil {
+			return NewLondonSigner(config.ChainID)
+		}
 		if config.BerlinBlock != nil {
 			return NewEIP2930Signer(config.ChainID)
 		}
@@ -82,7 +87,7 @@ func LatestSignerForChainID(chainID *big.Int) Signer {
 	if chainID == nil {
 		return HomesteadSigner{}
 	}
-	return NewEIP2930Signer(chainID)
+	return NewLondonSigner(chainID)
 }
 
 // SignTx signs the transaction using the given signer and private key.
@@ -165,6 +170,72 @@ type Signer interface {
 	Equal(Signer) bool
 }
 
+type londonSigner struct{ eip2930Signer }
+
+// NewLondonSigner returns a signer that accepts
+// - EIP-1559 dynamic fee transactions
+// - EIP-2930 access list transactions,
+// - EIP-155 replay protected transactions, and
+// - legacy Homestead transactions.
+func NewLondonSigner(chainId *big.Int) Signer {
+	return londonSigner{eip2930Signer{NewEIP155Signer(chainId)}}
+}
+
+func (s londonSigner) Sender(tx *Transaction) (common.Address, error) {
+	if tx.Type() != DynamicFeeTxType {
+		return s.eip2930Signer.Sender(tx)
+	}
+	V, R, S := tx.RawSignatureValues()
+	// DynamicFee txs are defined to use 0 and 1 as their recovery
+	// id, add 27 to become equivalent to unprotected Homestead signatures.
+	V = new(big.Int).Add(V, big.NewInt(27))
+	if tx.ChainId().Cmp(s.chainId) != 0 {
+		return common.Address{}, ErrInvalidChainId
+	}
+	return recoverPlain(s.Hash(tx), R, S, V, true)
+}
+
+func (s londonSigner) Equal(s2 Signer) bool {
+	x, ok := s2.(londonSigner)
+	return ok && x.chainId.Cmp(s.chainId) == 0
+}
+
+func (s londonSigner) SignatureValues(tx *Transaction, sig []byte) (R, S, V *big.Int, err error) {
+	txdata, ok := tx.inner.(*DynamicFeeTx)
+	if !ok {
+		return s.eip2930Signer.SignatureValues(tx, sig)
+	}
+	// Check that chain ID of tx matches the signer. We also accept ID zero here,
+	// because it indicates that the chain ID was not specified in the tx.
+	if txdata.ChainID.Sign() != 0 && txdata.ChainID.Cmp(s.chainId) != 0 {
+		return nil, nil, nil, ErrInvalidChainId
+	}
+	R, S, _ = decodeSignature(sig)
+	V = big.NewInt(int64(sig[64]))
+	return R, S, V, nil
+}
+
+// Hash returns the hash to be signed by the sender.
+// It does not uniquely identify the transaction.
+func (s londonSigner) Hash(tx *Transaction) common.Hash {
+	if tx.Type() != DynamicFeeTxType {
+		return s.eip2930Signer.Hash(tx)
+	}
+	return prefixedRlpHash(
+		tx.Type(),
+		[]interface{}{
+			s.chainId,
+			tx.Nonce(),
+			tx.Tip(),
+			tx.FeeCap(),
+			tx.Gas(),
+			tx.To(),
+			tx.Value(),
+			tx.Data(),
+			tx.AccessList(),
+		})
+}
+
 type eip2930Signer struct{ EIP155Signer }
 
 // NewEIP2930Signer returns a signer that accepts EIP-2930 access list transactions,
@@ -192,8 +263,8 @@ func (s eip2930Signer) Sender(tx *Transaction) (common.Address, error) {
 		V = new(big.Int).Sub(V, s.chainIdMul)
 		V.Sub(V, big8)
 	case AccessListTxType:
-		// ACL txs are defined to use 0 and 1 as their recovery id, add
-		// 27 to become equivalent to unprotected Homestead signatures.
+		// AL txs are defined to use 0 and 1 as their recovery
+		// id, add 27 to become equivalent to unprotected Homestead signatures.
 		V = new(big.Int).Add(V, big.NewInt(27))
 	default:
 		return common.Address{}, ErrTxTypeNotSupported
diff --git a/core/vm/eips.go b/core/vm/eips.go
index 025502760b4a1c22b3115c6a2ed6200d0c84bb70..4070a2db534285f031225de8398bda94d49d7476 100644
--- a/core/vm/eips.go
+++ b/core/vm/eips.go
@@ -26,6 +26,7 @@ import (
 
 var activators = map[int]func(*JumpTable){
 	3529: enable3529,
+	3198: enable3198,
 	2929: enable2929,
 	2200: enable2200,
 	1884: enable1884,
@@ -154,3 +155,22 @@ func enable3529(jt *JumpTable) {
 	jt[SSTORE].dynamicGas = gasSStoreEIP3529
 	jt[SELFDESTRUCT].dynamicGas = gasSelfdestructEIP3529
 }
+
+// enable3198 applies EIP-3198 (BASEFEE Opcode)
+// - Adds an opcode that returns the current block's base fee.
+func enable3198(jt *JumpTable) {
+	// New opcode
+	jt[BASEFEE] = &operation{
+		execute:     opBaseFee,
+		constantGas: GasQuickStep,
+		minStack:    minStack(0, 1),
+		maxStack:    maxStack(0, 1),
+	}
+}
+
+// opBaseFee implements BASEFEE opcode
+func opBaseFee(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
+	baseFee, _ := uint256.FromBig(interpreter.evm.Context.BaseFee)
+	scope.Stack.push(baseFee)
+	return nil, nil
+}
diff --git a/core/vm/evm.go b/core/vm/evm.go
index 8e3c9fe00f8ddd836536ce8cd4dfd439721f3094..980dc6d20154739344d1d105f74950aa77611db7 100644
--- a/core/vm/evm.go
+++ b/core/vm/evm.go
@@ -93,6 +93,7 @@ type BlockContext struct {
 	BlockNumber *big.Int       // Provides information for NUMBER
 	Time        *big.Int       // Provides information for TIME
 	Difficulty  *big.Int       // Provides information for DIFFICULTY
+	BaseFee     *big.Int       // Provides information for BASEFEE
 }
 
 // TxContext provides the EVM with information about a transaction.
diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go
index a0609a0d7894ea5cd25883d81d6ee40b96c58fbe..329ad77cbf835a2dab46d24bd77330d249358f9c 100644
--- a/core/vm/jump_table.go
+++ b/core/vm/jump_table.go
@@ -68,6 +68,7 @@ type JumpTable [256]*operation
 func newLondonInstructionSet() JumpTable {
 	instructionSet := newBerlinInstructionSet()
 	enable3529(&instructionSet) // EIP-3529: Reduction in refunds https://eips.ethereum.org/EIPS/eip-3529
+	enable3198(&instructionSet) // Base fee opcode https://eips.ethereum.org/EIPS/eip-3198
 	return instructionSet
 }
 
diff --git a/core/vm/opcodes.go b/core/vm/opcodes.go
index b0adf37d0c2698b788a2c2af7b2be0f6270ed04f..286307ae91aea9f269522b8d9ddb7b8def5f12b9 100644
--- a/core/vm/opcodes.go
+++ b/core/vm/opcodes.go
@@ -103,6 +103,7 @@ const (
 	GASLIMIT
 	CHAINID     OpCode = 0x46
 	SELFBALANCE OpCode = 0x47
+	BASEFEE     OpCode = 0x48
 )
 
 // 0x50 range - 'storage' and execution.
@@ -280,6 +281,7 @@ var opCodeToString = map[OpCode]string{
 	GASLIMIT:    "GASLIMIT",
 	CHAINID:     "CHAINID",
 	SELFBALANCE: "SELFBALANCE",
+	BASEFEE:     "BASEFEE",
 
 	// 0x50 range - 'storage' and execution.
 	POP: "POP",
@@ -432,6 +434,7 @@ var stringToOp = map[string]OpCode{
 	"CALLDATASIZE":   CALLDATASIZE,
 	"CALLDATACOPY":   CALLDATACOPY,
 	"CHAINID":        CHAINID,
+	"BASEFEE":        BASEFEE,
 	"DELEGATECALL":   DELEGATECALL,
 	"STATICCALL":     STATICCALL,
 	"CODESIZE":       CODESIZE,
diff --git a/eth/state_accessor.go b/eth/state_accessor.go
index 84cfaf4d738d80b5390d7506221662b66262e729..8d53739721fbd874f8a3d3f22ba987e87192f286 100644
--- a/eth/state_accessor.go
+++ b/eth/state_accessor.go
@@ -162,7 +162,7 @@ func (eth *Ethereum) stateAtTransaction(block *types.Block, txIndex int, reexec
 	signer := types.MakeSigner(eth.blockchain.Config(), block.Number())
 	for idx, tx := range block.Transactions() {
 		// Assemble the transaction call message and return if the requested offset
-		msg, _ := tx.AsMessage(signer)
+		msg, _ := tx.AsMessage(signer, block.BaseFee())
 		txContext := core.NewEVMTxContext(msg)
 		context := core.NewEVMBlockContext(block.Header(), eth.blockchain, nil)
 		if idx == txIndex {
diff --git a/eth/tracers/api.go b/eth/tracers/api.go
index 1c727f1366c20c57d9a5b48b808f66a4048b0df5..172054e9b8eecff4fcbbda1894974ad687ca4a36 100644
--- a/eth/tracers/api.go
+++ b/eth/tracers/api.go
@@ -271,7 +271,7 @@ func (api *API) traceChain(ctx context.Context, start, end *types.Block, config
 				blockCtx := core.NewEVMBlockContext(task.block.Header(), api.chainContext(localctx), nil)
 				// Trace all the transactions contained within
 				for i, tx := range task.block.Transactions() {
-					msg, _ := tx.AsMessage(signer)
+					msg, _ := tx.AsMessage(signer, task.block.BaseFee())
 					txctx := &txTraceContext{
 						index: i,
 						hash:  tx.Hash(),
@@ -523,7 +523,7 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac
 			defer pend.Done()
 			// Fetch and execute the next transaction trace tasks
 			for task := range jobs {
-				msg, _ := txs[task.index].AsMessage(signer)
+				msg, _ := txs[task.index].AsMessage(signer, block.BaseFee())
 				txctx := &txTraceContext{
 					index: task.index,
 					hash:  txs[task.index].Hash(),
@@ -545,7 +545,7 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac
 		jobs <- &txTraceTask{statedb: statedb.Copy(), index: i}
 
 		// Generate the next state snapshot fast without tracing
-		msg, _ := tx.AsMessage(signer)
+		msg, _ := tx.AsMessage(signer, block.BaseFee())
 		statedb.Prepare(tx.Hash(), block.Hash(), i)
 		vmenv := vm.NewEVM(blockCtx, core.NewEVMTxContext(msg), statedb, api.backend.ChainConfig(), vm.Config{})
 		if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas())); err != nil {
@@ -630,7 +630,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block
 	for i, tx := range block.Transactions() {
 		// Prepare the trasaction for un-traced execution
 		var (
-			msg, _    = tx.AsMessage(signer)
+			msg, _    = tx.AsMessage(signer, block.BaseFee())
 			txContext = core.NewEVMTxContext(msg)
 			vmConf    vm.Config
 			dump      *os.File
diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go
index 4c0240cd2c5b3ace879414e061a3f2552fa58fa9..24bce320cf00193f9dcc02cc546a9bb776736dde 100644
--- a/eth/tracers/api_test.go
+++ b/eth/tracers/api_test.go
@@ -161,7 +161,7 @@ func (b *testBackend) StateAtTransaction(ctx context.Context, block *types.Block
 	// Recompute transactions up to the target index.
 	signer := types.MakeSigner(b.chainConfig, block.Number())
 	for idx, tx := range block.Transactions() {
-		msg, _ := tx.AsMessage(signer)
+		msg, _ := tx.AsMessage(signer, block.BaseFee())
 		txContext := core.NewEVMTxContext(msg)
 		context := core.NewEVMBlockContext(block.Header(), b.chain, nil)
 		if idx == txIndex {
diff --git a/eth/tracers/tracers_test.go b/eth/tracers/tracers_test.go
index 9dc4c696311c0ef444cce6ade6cb19b7f313a3c4..8b01edd7b4cc45ae29b9473bc352eed546da287c 100644
--- a/eth/tracers/tracers_test.go
+++ b/eth/tracers/tracers_test.go
@@ -179,7 +179,7 @@ func TestPrestateTracerCreate2(t *testing.T) {
 	}
 	evm := vm.NewEVM(context, txContext, statedb, params.MainnetChainConfig, vm.Config{Debug: true, Tracer: tracer})
 
-	msg, err := tx.AsMessage(signer)
+	msg, err := tx.AsMessage(signer, nil)
 	if err != nil {
 		t.Fatalf("failed to prepare transaction for tracing: %v", err)
 	}
@@ -254,7 +254,7 @@ func TestCallTracer(t *testing.T) {
 			}
 			evm := vm.NewEVM(context, txContext, statedb, test.Genesis.Config, vm.Config{Debug: true, Tracer: tracer})
 
-			msg, err := tx.AsMessage(signer)
+			msg, err := tx.AsMessage(signer, nil)
 			if err != nil {
 				t.Fatalf("failed to prepare transaction for tracing: %v", err)
 			}
diff --git a/interfaces.go b/interfaces.go
index afcdc17e5824c436f68a7c108294f0acabd458e0..857d309be9c650e04f3ea5b59ece58be9a62879d 100644
--- a/interfaces.go
+++ b/interfaces.go
@@ -120,6 +120,9 @@ type CallMsg struct {
 	Value    *big.Int        // amount of wei sent along with the call
 	Data     []byte          // input data, usually an ABI-encoded contract method invocation
 
+	FeeCap *big.Int // EIP-1559 fee cap per gas.
+	Tip    *big.Int // EIP-1559 tip per gas.
+
 	AccessList types.AccessList // EIP-2930 access list.
 }
 
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index fe3f80c038c0eb5fce736c19a34eefc593513b64..7bc0477bd2738ab3b46ec6ba323a6a82d30cc81d 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -799,7 +799,7 @@ func (args *CallArgs) ToMessage(globalGasCap uint64) types.Message {
 		accessList = *args.AccessList
 	}
 
-	msg := types.NewMessage(addr, args.To, 0, value, gas, gasPrice, data, accessList, false)
+	msg := types.NewMessage(addr, args.To, 0, value, gas, gasPrice, nil, nil, data, accessList, false)
 	return msg
 }
 
@@ -1271,7 +1271,7 @@ func newRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber
 		result.BlockNumber = (*hexutil.Big)(new(big.Int).SetUint64(blockNumber))
 		result.TransactionIndex = (*hexutil.Uint64)(&index)
 	}
-	if tx.Type() == types.AccessListTxType {
+	if tx.Type() != types.LegacyTxType {
 		al := tx.AccessList()
 		result.Accesses = &al
 		result.ChainID = (*hexutil.Big)(tx.ChainId())
@@ -1393,7 +1393,7 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH
 		}
 		// Copy the original db so we don't modify it
 		statedb := db.Copy()
-		msg := types.NewMessage(args.From, args.To, uint64(*args.Nonce), args.Value.ToInt(), uint64(*args.Gas), args.GasPrice.ToInt(), input, accessList, false)
+		msg := types.NewMessage(args.From, args.To, uint64(*args.Nonce), args.Value.ToInt(), uint64(*args.Gas), args.GasPrice.ToInt(), nil, nil, input, accessList, false)
 
 		// Apply the transaction with the access list tracer
 		tracer := vm.NewAccessListTracer(accessList, args.From, to, precompiles)
diff --git a/les/odr_test.go b/les/odr_test.go
index 0c75014d4975158370995b2b291f07cf8dcfe1e8..5fb881b5c48f8def887ceae20be9900bf0fff8da 100644
--- a/les/odr_test.go
+++ b/les/odr_test.go
@@ -135,7 +135,7 @@ func odrContractCall(ctx context.Context, db ethdb.Database, config *params.Chai
 				from := statedb.GetOrNewStateObject(bankAddr)
 				from.SetBalance(math.MaxBig256)
 
-				msg := callmsg{types.NewMessage(from.Address(), &testContractAddr, 0, new(big.Int), 100000, new(big.Int), data, nil, false)}
+				msg := callmsg{types.NewMessage(from.Address(), &testContractAddr, 0, new(big.Int), 100000, new(big.Int), nil, nil, data, nil, false)}
 
 				context := core.NewEVMBlockContext(header, bc, nil)
 				txContext := core.NewEVMTxContext(msg)
@@ -150,7 +150,7 @@ func odrContractCall(ctx context.Context, db ethdb.Database, config *params.Chai
 			header := lc.GetHeaderByHash(bhash)
 			state := light.NewState(ctx, header, lc.Odr())
 			state.SetBalance(bankAddr, math.MaxBig256)
-			msg := callmsg{types.NewMessage(bankAddr, &testContractAddr, 0, new(big.Int), 100000, new(big.Int), data, nil, false)}
+			msg := callmsg{types.NewMessage(bankAddr, &testContractAddr, 0, new(big.Int), 100000, new(big.Int), nil, nil, data, nil, false)}
 			context := core.NewEVMBlockContext(header, lc, nil)
 			txContext := core.NewEVMTxContext(msg)
 			vmenv := vm.NewEVM(context, txContext, state, config, vm.Config{})
diff --git a/les/state_accessor.go b/les/state_accessor.go
index af5df36508ec0078221f3baee113e3a9c19b3912..e276b06dc748693580a158160c8feb33309e3d82 100644
--- a/les/state_accessor.go
+++ b/les/state_accessor.go
@@ -55,7 +55,7 @@ func (leth *LightEthereum) stateAtTransaction(ctx context.Context, block *types.
 	signer := types.MakeSigner(leth.blockchain.Config(), block.Number())
 	for idx, tx := range block.Transactions() {
 		// Assemble the transaction call message and return if the requested offset
-		msg, _ := tx.AsMessage(signer)
+		msg, _ := tx.AsMessage(signer, block.BaseFee())
 		txContext := core.NewEVMTxContext(msg)
 		context := core.NewEVMBlockContext(block.Header(), leth.blockchain, nil)
 		statedb.Prepare(tx.Hash(), block.Hash(), idx)
diff --git a/light/odr_test.go b/light/odr_test.go
index 0fc45b873418f18d3a90fc25fe63253d8a405721..bb47c69eb14a331193a293106217cb27278850c1 100644
--- a/light/odr_test.go
+++ b/light/odr_test.go
@@ -194,7 +194,7 @@ func odrContractCall(ctx context.Context, db ethdb.Database, bc *core.BlockChain
 
 		// Perform read-only call.
 		st.SetBalance(testBankAddress, math.MaxBig256)
-		msg := callmsg{types.NewMessage(testBankAddress, &testContractAddr, 0, new(big.Int), 1000000, new(big.Int), data, nil, false)}
+		msg := callmsg{types.NewMessage(testBankAddress, &testContractAddr, 0, new(big.Int), 1000000, new(big.Int), nil, nil, data, nil, false)}
 		txContext := core.NewEVMTxContext(msg)
 		context := core.NewEVMBlockContext(header, chain, nil)
 		vmenv := vm.NewEVM(context, txContext, st, config, vm.Config{})
diff --git a/params/bootnodes.go b/params/bootnodes.go
index 20bf0b7cbf2778d26598294df3f7ad10cb6ed9d6..99068750fce0358ac216c386ef26ec4b8aa588a0 100644
--- a/params/bootnodes.go
+++ b/params/bootnodes.go
@@ -69,9 +69,8 @@ var GoerliBootnodes = []string{
 
 // BaikalBootnodes are the enode URLs of the P2P bootstrap nodes running on the
 // Baikal ephemeral test network.
-// TODO: Set Baikal bootnodes
 var BaikalBootnodes = []string{
-	"",
+	"enode://9e1096aa59862a6f164994cb5cb16f5124d6c992cdbf4535ff7dea43ea1512afe5448dca9df1b7ab0726129603f1a3336b631e4d7a1a44c94daddd03241587f9@3.9.20.133:30303",
 }
 
 var V5Bootnodes = []string{
diff --git a/params/config.go b/params/config.go
index 2cafa0e44922cd6dc6394802959af6f917c04365..c777172381cc1c3c6cfd9ab7760f0abc4345a67f 100644
--- a/params/config.go
+++ b/params/config.go
@@ -231,7 +231,7 @@ var (
 		IstanbulBlock:       big.NewInt(0),
 		MuirGlacierBlock:    nil,
 		BerlinBlock:         big.NewInt(0),
-		LondonBlock:         big.NewInt(0),
+		LondonBlock:         big.NewInt(500),
 		Clique: &CliqueConfig{
 			Period: 30,
 			Epoch:  30000,
diff --git a/params/protocol_params.go b/params/protocol_params.go
index 22b4c0651c8f313875e1d9b45cf0774d192c8272..a49c4489f19f66e75749502936c6bbf909da404f 100644
--- a/params/protocol_params.go
+++ b/params/protocol_params.go
@@ -118,6 +118,10 @@ const (
 	// Introduced in Tangerine Whistle (Eip 150)
 	CreateBySelfdestructGas uint64 = 25000
 
+	BaseFeeChangeDenominator = 8          // Bounds the amount the base fee can change between blocks.
+	ElasticityMultiplier     = 2          // Bounds the maximum gas limit an EIP-1559 block may have.
+	InitialBaseFee           = 1000000000 // Initial base fee for EIP-1559 blocks.
+
 	MaxCodeSize = 24576 // Maximum bytecode to permit for a contract
 
 	// Precompiled contract gas prices
diff --git a/tests/init.go b/tests/init.go
index 240b7159d5c92415853e62e240bdf15c608b0304..b0a38e68b0740c332f302c56691c6f816ff669fe 100644
--- a/tests/init.go
+++ b/tests/init.go
@@ -179,6 +179,19 @@ var Forks = map[string]*params.ChainConfig{
 		BerlinBlock:         big.NewInt(0),
 		LondonBlock:         big.NewInt(0),
 	},
+	"Aleut": {
+		ChainID:             big.NewInt(1),
+		HomesteadBlock:      big.NewInt(0),
+		EIP150Block:         big.NewInt(0),
+		EIP155Block:         big.NewInt(0),
+		EIP158Block:         big.NewInt(0),
+		ByzantiumBlock:      big.NewInt(0),
+		ConstantinopleBlock: big.NewInt(0),
+		PetersburgBlock:     big.NewInt(0),
+		IstanbulBlock:       big.NewInt(0),
+		BerlinBlock:         big.NewInt(0),
+		LondonBlock:         big.NewInt(0),
+	},
 }
 
 // Returns the set of defined fork names
diff --git a/tests/state_test_util.go b/tests/state_test_util.go
index 46834de6daeb8e0b755c83bfdff8d55e71ff2eee..9778f058fe46dcf56eaab5af294982d6fc3ca47c 100644
--- a/tests/state_test_util.go
+++ b/tests/state_test_util.go
@@ -297,7 +297,7 @@ func (tx *stTransaction) toMessage(ps stPostState) (core.Message, error) {
 	if tx.AccessLists != nil && tx.AccessLists[ps.Indexes.Data] != nil {
 		accessList = *tx.AccessLists[ps.Indexes.Data]
 	}
-	msg := types.NewMessage(from, to, tx.Nonce, value, gasLimit, tx.GasPrice, data, accessList, true)
+	msg := types.NewMessage(from, to, tx.Nonce, value, gasLimit, tx.GasPrice, nil, nil, data, accessList, true)
 	return msg, nil
 }